18. Базы данных. Теория
«Транзакция — это преобразование состояния, которое обладает свойствами атомарности, согласованности, изолированности и долговечности».
— Джим Грей
Архитектурный фундамент транзакций: Модель ACID
flowchart TD
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
ACID["Гарантии ACID"]:::entry
ACID --> A["Atomicity (Атомарность): все операции фиксируются или все откатываются (WAL / Undo Log)"]:::process
ACID --> C["Consistency (Согласованность): сохранение инвариантов схемы, внешних ключей и check-ограничений"]:::accent
ACID --> I["Isolation (Изоляция): параллельные транзакции не видят незафиксированных мутаций (MVCC / Locks)"]:::process
ACID --> D["Durability (Долговечность): закоммиченные данные сохранены на энергонезависимый диск (fsync WAL)"]:::success
Уровни изоляции транзакций по стандарту ANSI SQL и аномалии
flowchart LR
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
L0["Read Uncommitted"]:::warning -->|"Защита от Dirty Read"| L1["Read Committed (Дефолт PostgreSQL)"]:::accent
L1 -->|"Защита от Non-Repeatable Read"| L2["Repeatable Read (Snapshot Isolation)"]:::process
L2 -->|"Защита от Phantom Read и Write Skew"| L3["Serializable (SSI / Строгая сериализуемость)"]:::success
Архитектуры движков хранения: B-Tree vs LSM-Tree
flowchart TD
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
subgraph BTree["B-Tree Engine (PostgreSQL, MySQL InnoDB, SQLite)"]
BWrite["Запись: модификация страниц in-place + случайный I/O (Random Writes)"]:::warning
BRead["Чтение: быстрый точечный lookup O(log N) за фиксированное число чтений страниц"]:::success
BComp["Оптимум: Read-Heavy OLTP с преобладанием выборок по первичным ключам"]:::accent
end
subgraph LSM["LSM-Tree Engine (Cassandra, RocksDB, ScyllaDB, ClickHouse)"]
LWrite["Запись: append-only в WAL + память MemTable -> последовательный I/O (Sequential)"]:::success
LCompaction["Фон: слияние уровней SSTable (Compaction) с усилением записи (Write Amplification)"]:::warning
LRead["Чтение: проверка MemTable + Bloom Filters + слияние уровней (Read Amplification)"]:::process
LUse["Оптимум: Write-Heavy нагрузки, сбор метрик, логи, Time-Series"]:::accent
end
Навигатор по вопросам главы
- Что такое ACID?
- Что такое транзакция?
- Что такое уровни изоляции (isolation levels)?
- Что такое индекс (index)?
- Что такое B-tree?
- Что такое MVCC?
- Что такое EXPLAIN и зачем он нужен?
- Что такое денормализация?
- Что такое партиционирование (partitioning)?
- Что такое шардирование (sharding)?
- Что такое репликация (replication)?
- Что такое CAP теорема?
- Что такое BASE?
- В чем разница между OLAP и OLTP?
- 1. Что такое NoSQL
- Чем NoSQL отличается от SQL?
- Что такое инвертированный индекс?
- Что такое блокировки (locks) и латчи (latches)?
- Что такое дедлок в базах данных?
- Как работают триггеры и когда их использовать?
- Что такое хранимые процедуры?
- Что такое "сериализуемость" (serializability)?
- Что такое «фантомное чтение» (phantom read)?
- Что такое «неповторяющееся чтение» (non-repeatable read)?
- Что такое «грязное чтение» (dirty read)?
- Что такое «читатель-писатель» блокировка?
- Что такое "журнал предзаписи" (WAL)?
- Как работает восстановление после сбоев?
- Что такое "консенсус" (Paxos/Raft)?
- Что такое "двухфазный коммит" (2PC)?
- Что такое Saga паттерн?
- Что такое Eventual Consistency?
- Что такое CDC (Change Data Capture)?
- Что такое Row-based vs Statement-based репликация?
- Что такое "callback hell" в контексте асинхронных операций с БД?
- Что такое "ORM" и "Raw SQL" – плюсы и минусы?
- Как работают "материализованные представления" (materialized views)?
- Что такое "сегмент" (segment) и "экстент" (extent) в физической организации БД?
- Что такое "partial index" и "covering index"?
- Как работает "сборка мусора" в MVCC (например, vacuum в PostgreSQL)?
- Как устроен "буферный пул" (buffer pool) и зачем он нужен?
- Что такое "time-series базы данных" и в чем их особенность?
- Что такое "графовые базы данных" и когда их использовать?
- Что такое "NewSQL" и какие задачи он решает?
- Как работает "лимит соединений" и как его правильно настроить?
- Что такое "connection pooling" и какие стратегии существуют?
- Как работает "failover" и "switchover"?
- Как спроектировать схему БД для хранения древовидных данных (adjacency list, nested set, materialized path)?
- Как работает "индекс Брина" (BRIN) и для каких данных он подходит?
Что такое ACID?
Краткий ответ (30–60 секунд)
ACID — это набор свойств транзакций, гарантирующих надёжную обработку данных: Атомарность (всё или ничего), Согласованность (переход из одного корректного состояния в другое), Изоляция (параллельные транзакции не мешают друг другу), Долговечность (зафиксированные изменения сохраняются даже при сбоях). Это фундамент реляционных СУБД, позволяющий рассматривать группу операций как единое целое и не терять данные.
Развёрнутый ответ
ACID образует контракт, который берёт на себя база данных при выполнении явной транзакции:
- Atomicity (Атомарность): обеспечивает неделимость набора операций. Если транзакция не завершилась успешно целиком, все её изменения откатываются (rollback). Реализуется через журнал предзаписи (WAL) и механизмы отмены. В Go с
database/sqlатомарность достигается методамиtx.Commit()/tx.Rollback(). - Consistency (Согласованность): после завершения транзакции данные удовлетворяют всем определённым ограничениям (первичные/внешние ключи, CHECK, триггеры), а инварианты приложения не нарушены. Фактически это свойство обеспечивается совместно приложением и декларативными правилами СУБД.
- Isolation (Изоляция): определяет, насколько сильно одна транзакция изолирована от эффектов других. Уровни изоляции (Read Uncommitted … Serializable) задают допустимые аномалии. Чем выше изоляция, тем меньше гонок, но и больше накладных расходов.
- Durability (Долговечность): подтверждённая (
COMMIT) транзакция переживёт отказ питания, крах сервера и т.п. Обычно гарантируется синхронной записью WAL на энергонезависимое хранилище перед возвратом успеха клиенту.
Без ACID пришлось бы вручную реализовывать логику откатов и проверок целостности в каждом приложении, что невероятно повышает риск потери и порчи данных.
Под капотом (internals)
- Atomicity + Durability -> ARIES (Algorithms for Recovery and Isolation Exploiting Semantics): в основе многих реляционных движков. WAL-записи содержат как «redo» (повторить), так и «undo» (отменить) информацию. При восстановлении проигрываются redo-записи для достижения состояния на момент краха, затем undo-записями откатываются незавершённые транзакции.
- Isolation: реализуется через блокировки (two-phase locking) или MVCC (как в PostgreSQL, InnoDB). Детали зависят от выбранного уровня изоляции.
- Consistency: проверяется на этапе выполнения операторов и при коммите; нарушение вызывает немедленный откат.
В database/sql драйвер передаёт команды BEGIN, COMMIT, ROLLBACK, а сама СУБД внутри использует вышеописанные механизмы.
Типичные ошибки кандидатов
- Путать атомарность с изоляцией: «атомарность гарантирует, что параллельные транзакции не видят промежуточных состояний». На самом деле это задача изоляции; атомарность лишь гарантирует, что при ошибке все изменения откатятся.
Как исправить: чётко разделять: атомарность — «всё или ничего» для одной транзакции, изоляция — про взаимодействие параллельных транзакций. - Считать, что ACID медленный и всегда нужен Serializable: многие думают, что ACID эквивалентен полной сериализуемости; но можно выбрать более лёгкие уровни изоляции, балансируя производительность и строгость гарантий.
Как исправить: правильно конфигурировать уровень изоляции под бизнес-требования, применяя Read Committed по умолчанию, а Serializable только для критичных операций.
Возможные дополнительные вопросы интервьюера
Чем ACID отличается от BASE?
ACID — сильная согласованность, ориентирован на целостность; BASE — слабая (eventually consistent), ориентирован на доступность. Применяются в SQL и NoSQL соответственно.
Может ли NoSQL-база быть ACID?
Некоторые NewSQL / распределённые БД (Spanner, CockroachDB) обеспечивают полную ACID поверх географически распределённых узлов.
Что такое транзакция?
Краткий ответ (30–60 секунд)
Транзакция — логическая единица работы с БД, состоящая из одной или нескольких операций чтения/записи. Её ключевое свойство: либо выполняются все операции и фиксируются (COMMIT), либо не выполняется ни одна (ROLLBACK). Транзакции превращают набор шагов в атомарное, согласованное, изолированное и долговечное изменение состояния.
Развёрнутый ответ
Транзакция моделирует бизнес-операцию, которую нельзя дробить. Пример: перевод денег между счетами — списание с одного и зачисление на другой; оба действия должны произойти или не произойти вовсе. В Go:
tx, _ := db.Begin()
_, err1 := tx.Exec("UPDATE accounts SET balance = balance - 100 WHERE id = 1")
_, err2 := tx.Exec("UPDATE accounts SET balance = balance + 100 WHERE id = 2")
if err1 != nil || err2 != nil {
tx.Rollback()
return
}
tx.Commit()
Границы транзакции задаются BEGIN и COMMIT/ROLLBACK. Внутри может быть сколько угодно операторов. База гарантирует изоляцию на уровне, указанном в настройках или выражении SET TRANSACTION ISOLATION LEVEL.
Под капотом (internals)
- Каждая транзакция получает уникальный возрастающий идентификатор (XID). Её изменения до
COMMITвидны только ей через приватную версию строк (MVCC) или блокировки. - В WAL пишутся все изменения, включая маркеры начала и конца транзакции. При сбое по WAL восстанавливается состояние до краха, а незакоммиченные транзакции откатываются.
- В распределённых системах транзакции могут быть распределёнными и требуют протоколов 2PC для атомарности между узлами.
Типичные ошибки кандидатов
- Смешение понятий «транзакция» и «соединение»: полагать, что транзакция привязана к TCP-соединению; на самом деле соединение может выполнять много последовательных транзакций, а некоторые пулы могут мигрировать транзакции.
Как исправить: понимать, что состояние транзакции в классических драйверах ассоциировано с сессией, но в распределённых БД оно может пережить разрыв соединения. - Не обрабатывать
Rollbackошибок:tx.Rollback()тоже может вернуть ошибку (например, соединение уже умерло), которую нужно логировать.
Как исправить: всегда проверять ошибкуRollbackи логировать её, но не паниковать.
Возможные дополнительные вопросы интервьюера
Влияет ли длительность транзакции на систему?
Долгие транзакции держат старые версии строк, распухает MVCC-мусор, растёт вероятность конфликтов; их стоит делать как можно короче.
Как обрабатывать бизнес-логику до коммита?
Никаких сайд-эффектов (отправка email) до успешного COMMIT, иначе при откате действие уже не отменить. Паттерн: отложенное выполнение после подтверждения.
Что такое уровни изоляции (isolation levels)?
Краткий ответ (30–60 секунд)
Уровни изоляции определяют, какие аномалии конкурентного доступа допускаются в транзакциях. Стандарт SQL определяет четыре уровня: Read Uncommitted, Read Committed, Repeatable Read, Serializable. Каждый последующий запрещает больше феноменов (грязное чтение, неповторяющееся чтение, фантомы) ценой больших накладных расходов.
Развёрнутый ответ
| Уровень | Dirty Read | Non-repeatable Read | Phantom Read |
|---|---|---|---|
| Read Uncommitted | Да | Да | Да |
| Read Committed | Нет | Да | Да |
| Repeatable Read | Нет | Нет | Да (в общем случае) |
| Serializable | Нет | Нет | Нет |
В PostgreSQL Repeatable Read реально не допускает фантомов благодаря SI (Snapshot Isolation); Serializable реализован через SSI (Serializable Snapshot Isolation). В MySQL InnoDB Repeatable Read тоже предотвращает фантомы (gap locks). Выбор уровня влияет на пропускную способность: многие системы по умолчанию используют Read Committed.
Под капотом (internals)
- Read Committed: каждый оператор видит снимок на момент начала оператора; блокировки строк удерживаются только на время модификации и отпускаются при коммите.
- Repeatable Read (Snapshot Isolation): транзакция видит снимок на момент своего начала; запись возможна только если строка не изменилась с момента снимка (проверка по версии — «first-committer-wins»).
- Serializable (SSI): дополнительно отслеживаются зависимости между транзакциями для выявления циклов, приводящих к аномалиям сериализации; при обнаружении одна из транзакций откатывается.
- Реализация: MVCC хранит несколько версий строк с метаданными о создавшей/удалившей транзакции.
Типичные ошибки кандидатов
- Использовать уровень Serializable для всего как «безопасный»: ведёт к частым откатам и падению производительности; надо выбирать минимально достаточный уровень.
Как исправить: анализировать бизнес-сценарий; большинству операций хватит Read Committed. - Полагать, что Repeatable Read в PostgreSQL исключает все фантомы: на самом деле определённые write-skew аномалии возможны, пока не включён Serializable.
Как исправить: изучить документацию конкретной СУБД; при необходимости использовать SELECT … FOR UPDATE для явных блокировок.
Возможные дополнительные вопросы интервьюера
Что такое write skew и на каком уровне изоляции он возникает?
Write skew — аномалия, при которой две транзакции читают пересекающиеся данные, но пишут в непересекающиеся, нарушая инвариант; возникает при Snapshot Isolation, устраняется только Serializability.
Почему в PostgreSQL Repeatable Read не блокирует строки, а использует обнаружение конфликтов?
PostgreSQL полагается на оптимистичный подход: конфликт обновления детектится при коммите и вызывает ошибку сериализации (40001), приложению надо повторить транзакцию.
Что такое индекс (index)?
Краткий ответ (30–60 секунд)
Индекс — структура данных, ускоряющая поиск строк в таблице по заданному набору полей (ключей). Основная идея: создать отображение значения ключа → физическое расположение строки, подобно указателю в книге. Индексы критически важны для производительности, но замедляют операции вставки и обновления, увеличивают объём хранилища.
Развёрнутый ответ
Индексы бывают нескольких типов:
- B-tree / B+tree — основной тип, поддерживает поиск по равенству и диапазону.
- Hash — только равенство, быстрее B-tree для точечных запросов, но не диапазоны.
- GiST, GIN — для полнотекстового поиска, геоданных, массивов.
- BRIN — для очень больших таблиц с физически отсортированными данными.
Индексы могут быть уникальными (как первичный ключ) или неуникальными. Составной индекс (по нескольким колонкам) упорядочен лексикографически; чтобы использовать индекс для порядка, важна префиксная часть. В Go-приложениях индексы прозрачны: драйвер просто отправляет SQL, а планировщик БД решает, какой индекс применить.
Под капотом (internals)
- Индекс хранит ключ и ссылку на строку (TID — tuple ID, состоящий из номера страницы и смещения). В PostgreSQL индекс отделён от кучи (heap), все индексы вторичны. В InnoDB первичный ключ кластеризован — строки хранятся в листьях B+tree самого PK.
- При вставке строки обновляются все индексы таблицы, поэтому избыток индексов замедляет вставки. Операции DELETE/UPDATE оставляют «мёртвые» записи в индексе, которые позже вычищаются (vacuum в PostgreSQL).
- Планировщик оценивает селективность индекса по статистике (гистограммы) и выбирает оптимальный план.
Типичные ошибки кандидатов
- Создавать индекс на каждую колонку «на всякий случай»: приводит к деградации скорости записи и нерациональному использованию памяти.
Как исправить: индексировать только колонки, участвующие в WHERE, JOIN, ORDER BY, и оценивать план запроса. - Полагать, что B-tree индекс всегда используется для
LIKE '%pattern%': нет, поиск с ведущим wildcard (%abc) не может использовать стандартный B-tree индекс, так как нарушается лексикографический порядок.
Как исправить: для таких задач применять полнотекстовые индексы или pg_trgm.
Возможные дополнительные вопросы интервьюера
Почему индекс не всегда ускоряет запрос?
Если выборка затрагивает большую долю таблицы, последовательное сканирование может быть дешевле из-за фрагментированного чтения.
Что такое покрывающий индекс (covering index)?
Индекс, содержащий все колонки, запрашиваемые запросом, позволяет избежать обращения к основной таблице.
Что такое B-tree?
Краткий ответ (30–60 секунд)
B-tree — самобалансирующееся дерево поиска с высокой степенью ветвления, оптимизированное для хранения на диске. Каждый узел содержит множество ключей и ссылок на дочерние узлы. B+tree — его вариант, где все данные хранятся только в листьях, а внутренние узлы содержат лишь ключи для навигации. Это основа большинства индексов реляционных БД.
Развёрнутый ответ
Свойства B-tree порядка m:
- Каждый узел (кроме корня) содержит от ⌈m/2⌉ до m ключей.
- Все листья находятся на одном уровне, гарантируя логарифмическое время доступа O(log n).
- Ключи отсортированы, поддерживается поиск по диапазону.
В базах данных используется B+tree:
- Листья сцеплены в двусвязный список для эффективного сканирования диапазона (Range Scan).
- Внутренние узлы – только как «карта» для навигации.
- Высокий branching factor (сотни ключей) минимизирует количество дисковых операций (I/O), потому что высота дерева редко превышает 3-4 даже для миллиардов записей.
В Go нет прямой реализации B-tree на уровне языка, но понимание его структуры необходимо для анализа планов запросов и выбора оптимальных индексов.
Под капотом (internals)
- В PostgreSQL B+tree хранит индексные кортежи, состоящие из значения ключа и TID (6 байт). Страницы по 8 КБ.
- При вставке, если узел переполнен, происходит расщепление (split): половина ключей переносится в новый узел, средний ключ поднимается в родительский узел.
- При удалении может выполняться слияние (merge) соседних узлов.
- Обход дерева начинается с корня в buffer cache; каждая посещённая страница может быть либо в памяти, либо считана с диска (случайный доступ). Поэтому важен хорошо настроенный буферный пул.
Типичные ошибки кандидатов
- Считать B-tree тем же, что и бинарное дерево: неправильно переносить свойства бинарных деревьев (2 дочерних узла) на B-tree с сотнями ветвей.
Как исправить: чётко различать: B-tree создано для минимизации дискового I/O, бинарные — для оперативной памяти. - Недооценивать влияние заполнения (fillfactor): при частых вставках в случайном порядке оставление 100% заполнения ведёт к частым расщеплениям.
Как исправить: выставлять fillfactor < 100 (например, 80-90) для активно обновляемых индексов.
Возможные дополнительные вопросы интервьюера
Почему в B+tree все листья обязательно на одном уровне?
Гарантирует предсказуемое количество дисковых операций для любой записи; это фундаментальное свойство самобалансировки.
Чем B+tree отличается от LSM-дерева?
B+tree оптимизировано для чтения и точечных обновлений; LSM-дерево — для интенсивной записи (append-only), используется в RocksDB, Cassandra.
Что такое MVCC?
Краткий ответ (30–60 секунд)
Multiversion Concurrency Control (MVCC) — механизм управления конкурентным доступом, при котором каждая транзакция видит «снимок» (snapshot) данных на определённый момент времени. Вместо блокировок строк при чтении, MVCC создаёт новую версию строки при изменении. Это позволяет читателям не блокировать писателей и наоборот, увеличивая пропускную способность.
Развёрнутый ответ
Основная идея: каждая строка в таблице может иметь несколько версий (tuple versions). При запросе транзакция получает список версий, из которых видимы те, что были зафиксированы до начала её snapshot’а. Обновление приводит к вставке новой версии и пометке старой как устаревшей.
Преимущества:
- Читатели никогда не ждут писателей (нет разделяемых блокировок).
- Откат транзакции мгновенен (просто отбрасываются новые версии).
Используется в PostgreSQL (heap + tuple visibility), InnoDB (undo log), Oracle. В Go-коде драйвер не управляет MVCC напрямую, но разработчик должен понимать, почему SELECT без FOR UPDATE видит «старые» данные и как это влияет на бизнес-логику.
Под капотом (internals)
- PostgreSQL: каждая строка содержит системные колонки
xmin(XID создавшей транзакции),xmax(XID удалившей/заменившей транзакции) иctid(физический адрес). При обновлении создаётся новый кортеж сxmin= текущий XID, а у старогоxmax= текущий XID. Видимость определяется по снимку активных транзакций и уровню изоляции. - InnoDB: хранит старые версии в отдельном undo tablespace; в строке есть указатель на цепочку undo-записей. При чтении строится согласованная версия, откатывая изменения неприменённых транзакций.
- Очистка старых версий (vacuum в PG, purge thread в InnoDB) удаляет кортежи, которые уже не видны ни одной активной транзакции.
Типичные ошибки кандидатов
- Полагать, что MVCC решает все проблемы конкурентности: MVCC действительно снимает конфликты читателей/писателей, но не защищает от write skew и аномалий потерянного обновления (lost update).
Как исправить: применять явные блокировки (SELECT FOR UPDATE) или Serializable уровень для критических секций. - Забывать, что длинные транзакции удлиняют жизнь старых версий: если транзакция держится часами, версии строк, созданные до её начала, не могут быть удалены, что ведёт к раздуванию таблицы.
Как исправить: минимизировать продолжительность транзакций, использоватьidle_in_transaction_session_timeout.
Возможные дополнительные вопросы интервьюера
В чём разница MVCC в PostgreSQL и MySQL InnoDB?
В PostgreSQL нет отдельного undo-сегмента; старые версии хранятся прямо в тех же страницах кучи. InnoDB выносит undo в отдельное табличное пространство.
Как MVCC влияет на сборку мусора в БД?
Порождает необходимость регулярного vacuum для возврата места; в InnoDB — фоновый purge поток удаляет старые версии из undo log.
Что такое EXPLAIN и зачем он нужен?
Краткий ответ (30–60 секунд)
EXPLAIN — команда, показывающая план выполнения запроса, выбранный планировщиком СУБД. Она раскрывает, какие индексы используются, как объединяются таблицы (nested loop, hash join, merge join), оценивает стоимость и количество строк. Критически важна для оптимизации медленных запросов и понимания, почему индекс не используется.
Развёрнутый ответ
EXPLAIN (или EXPLAIN ANALYZE, который реально выполняет запрос и собирает фактические метрики) выводит дерево операций. Пример в PostgreSQL:
EXPLAIN SELECT * FROM orders WHERE customer_id = 42 AND order_date > '2025-01-01';
может показать:
- Index Scan по индексу на
customer_id, - Фильтр по
order_date(если индекс составной, может быть Index Cond).
Анализ показывает cost (startup и total), rows (оценочное число строк), loops.
В Go можно использовать rows, _ := db.Query("EXPLAIN SELECT ...") для получения плана в коде при отладке. Инструменты типа pg_stat_statements собирают статистику по самым дорогим запросам.
Под капотом (internals)
- Планировщик разбивает запрос на реляционные операторы (сканирование, соединение, aggregation) и для каждого оценивает стоимость CPU и I/O на основе статистики (pg_stats).
- Использует динамическое программирование для выбора порядка соединений. Полученный план транслируется в исполнимый код (executor).
EXPLAIN ANALYZEдополнительно запускает executor и собирает реальные времена и число операций, что позволяет увидеть ошибки оценки селективности.
Типичные ошибки кандидатов
- Игнорирование
ANALYZE: безANALYZEвы видите только оценки планировщика, которые могут быть далеки от реальности при устаревшей статистике.
Как исправить: использоватьEXPLAIN (ANALYZE, BUFFERS)на тестовой базе с репрезентативными данными. - Смотреть только на общий cost, не сравнивая с реальным временем: cost — условная единица; важен реальный
Execution Timeи использование буферов.
Как исправить: сравниватьactual timeиbuffers: shared hit/readмежду разными версиями индекса.
Возможные дополнительные вопросы интервьюера
Что означает Seq Scan при большом rows?
Это последовательное сканирование всей таблицы; если запрос фильтрует малую долю строк, скорее всего отсутствует подходящий индекс.
Почему планировщик иногда игнорирует индекс, хотя он есть?
Из-за низкой селективности (предикат возвращает >5-10% таблицы) или устаревшей статистики; помогает ANALYZE table.
Что такое денормализация?
Краткий ответ (30–60 секунд)
Денормализация — намеренное внесение избыточности в реляционную схему ради повышения производительности чтения. Это обратный процесс к нормализации: мы дублируем данные или предварительно вычисляем агрегаты, чтобы избежать сложных JOIN-ов и сократить время запросов. Платим ценой усложнения обновлений и риска аномалий данных.
Развёрнутый ответ
Пример: вместо нормализованной схемы orders -> customers можно добавить поле customer_name прямо в таблицу orders. Это ускорит отображение списка заказов (без JOIN), но при изменении имени клиента потребует обновления всех его заказов.
Применяют в:
- Отчётных (OLAP) системах, где избыточность оправдана.
- Кэшировании денормализованных представлений через материализованные представления.
- Высоконагруженных приложениях, где JOIN слишком дорог.
В Go, взаимодействуя с БД, нужно продумывать стратегию синхронизации денормализованных данных: либо триггерами/правилами в БД, либо на уровне приложения (event-driven с использованием CDC).
Под капотом (internals)
- На уровне СУБД нет специального механизма «денормализации» — это проектировочное решение.
- При денормализации возрастает нагрузка на WAL при массовых обновлениях продублированных полей, растёт размер и фрагментация.
- Контроль целостности полностью ложится на разработчика (нет foreign key).
Типичные ошибки кандидатов
- Преждевременная денормализация без замеров: начинают дублировать данные, не убедившись, что узким местом действительно является JOIN.
Как исправить: сначала оптимизировать запрос (индексы, покрывающие индексы), измерить, и только потом принимать решение. - Не обеспечивать согласованность копий: после обновления исходной сущности забывают обновить денормализованные поля, получая расхождение.
Как исправить: использовать каскадное обновление через триггеры или реализовать eventual consistency с CDC и повторной обработкой.
Возможные дополнительные вопросы интервьюера
Чем денормализация отличается от создания представления (view)?
Представление — это «виртуальная таблица», которая при обращении выполняет запрос, не хранит данные; денормализация физически дублирует данные.
Когда денормализация категорически неприемлема?
В системах с жёсткими требованиями целостности (финансовые транзакции), где любое расхождение недопустимо.
Что такое партиционирование (partitioning)?
Краткий ответ (30–60 секунд)
Партиционирование — декларативное разделение большой таблицы на непересекающиеся физические сегменты (партиции) по ключу (дата, ID). Запросы, содержащие условие по ключу, сканируют только нужные партиции (partition pruning), что значительно снижает I/O. Каждая партиция может храниться на разных дисковых томах.
Развёрнутый ответ
Виды партиционирования в PostgreSQL:
- Range Partitioning: по диапазонам значений (например, по месяцам).
- List Partitioning: по перечислению (по странам).
- Hash Partitioning: по хеш-функции для равномерного распределения.
Преимущества:
- Быстрое удаление старых данных (
DROP PARTITIONмгновенен). - Улучшение параллелизма при обслуживании.
- Локальные индексы для каждой партиции.
В Go-приложении прозрачно — SQL тот же, но стоит учитывать, что при вставке нужно вычислять партицию. Лучшие практики: использовать партиционирование по дате для логов, событий, телеметрии.
Под капотом (internals)
- В PostgreSQL 10+ поддерживается декларативное партиционирование. Родительская таблица становится пустой; каждая партиция — отдельная таблица с ограничением
CHECK. - Планировщик использует pruning: исключает партиции, которые не могут содержать нужные данные.
- Индексы могут быть локальными (на каждой партиции отдельно) или глобальными (пока не поддерживаются в PG, только декларативно).
- Вставка с ключом вне диапазонов вызывает ошибку; есть автоматическое перемещение строк при обновлении ключа партиционирования (row movement).
Типичные ошибки кандидатов
- Использование партиционирования как замены индексам: думать, что разбиение на партиции само ускорит точечные запросы, но без индекса на партиции это всё равно Seq Scan на каждую подходящую партицию.
Как исправить: обязательно создавать индексы на каждой партиции. - Неправильный выбор ключа, приводящий к «перекосу» (hot spot): партиционирование по ID с возрастающим ключом равномерно, но все записи идут в последнюю партицию, что может не дать выигрыша от параллелизма записи.
Как исправить: для записи использовать hash-партиционирование или выбирать ключ, равномерно распределяющий записи.
Возможные дополнительные вопросы интервьюера
Чем партиционирование отличается от шардирования?
Партиционирование — деление одной таблицы внутри одного сервера БД; шардирование — разделение данных по разным серверам (шардам).
Можно ли сделать партиционирование прозрачно для приложения?
Да, SQL-интерфейс не меняется; ORM могут потребовать ручной маршрутизации, но чистый SQL скрывает детали.
Что такое шардирование (sharding)?
Краткий ответ (30–60 секунд)
Шардирование — горизонтальное масштабирование базы данных путём разделения данных на независимые части (шарды), расположенные на разных физических серверах. Каждый шард содержит подмножество строк, разбитых по ключу шардирования (tenant ID, хеш от пользователя). Позволяет обрабатывать больше запросов и данных, чем вмещает один сервер.
Развёрнутый ответ
Шардирование часто реализуется на уровне приложения: клиентский код (или прокси) выбирает, к какому шарду обращаться, на основе ключа. В Go это может выглядеть как:
shardID := hash(userID) % numShards
db := shardPool[shardID]
db.Query("SELECT ... WHERE user_id = ?", userID)
Проблемы: неравномерное распределение (shard skew), сложность выполнения кросс-шардовых запросов и транзакций. Для решения применяют CQRS, агрегацию на стороне приложения или используют готовые решения (Vitess, Citus).
Под капотом (internals)
- Каждый шард — независимый экземпляр БД со своей схемой. Распределённые запросы требуют координатора (Citus для PostgreSQL).
- Для перебалансировки применяются виртуальные шарды (vshards) — число шардов > числа узлов, что облегчает миграцию.
- Обеспечение ACID между шардами невозможно без 2PC, что дорого; на практике предпочитают BASE.
Типичные ошибки кандидатов
- Раннее введение шардирования: многие пытаются шардироваться на 100 ГБ данных, хотя один сервер с репликацией справляется на порядок больше.
Как исправить: исчерпать возможности вертикального масштабирования и репликации, и только потом идти в шарды. - Неверный выбор ключа шардирования, ведущий к горячему шарду (hot shard): весь трафик крупного клиента попадает на один шард.
Как исправить: использовать многомерное шардирование или шардирование по нескольким полям (например, tenant+date).
Возможные дополнительные вопросы интервьюера
Как выполнять JOIN между шардами?
На стандартной реляционной БД — только вытаскивая данные в приложение и объединяя там, либо используя распределённые движки (Citus), которые умеют передавать данные.
Как обрабатывать уникальные глобальные ID при шардировании?
Использовать UUID, Snowflake ID, или централизованный сервис последовательностей.
Что такое репликация (replication)?
Краткий ответ (30–60 секунд)
Репликация — копирование данных с одного сервера БД (мастера/primary) на один или несколько подчинённых (реплик/standby) для повышения доступности и масштабирования чтения. Изменения передаются в виде потока журнала (WAL) или логических записей. При сбое мастера одна из реплик может быть повышена до новой первичной (failover).
Развёрнутый ответ
Виды репликации:
- Физическая (physical): передача постраничных изменений WAL. Быстро, но реплика должна иметь ту же архитектуру и версию.
- Логическая (logical): передача изменений на уровне строк (INSERT/UPDATE/DELETE). Позволяет реплицировать между разными версиями, частичную репликацию.
Конфигурации: синхронная (мастер ждёт подтверждения от реплики) — гарантия отсутствия потерь, но задержка; асинхронная — выше производительность, возможна потеря данных при аварии.
В Go-приложениях часто делают чтение с реплик (replica.Db), запись — на мастер. Важно помнить о replication lag и применять стратегии типа «read-your-writes» (кэширование своего же изменения) или направлять чтение на мастер в рамках транзакции.
Под капотом (internals)
- PostgreSQL: WAL-записи из
pg_walотправляются процессом WAL sender. Реплика получает и записывает в свой WAL (приrecovery), затем применяет изменения. Синхронный режим используетsynchronous_standby_names. - MySQL: асинхронная/полусинхронная репликация через binlog (Row-based или Statement-based).
- При повышении реплики до мастер (promote) выполняется recovery до конца доступного WAL, после чего сервер переходит в нормальный режим.
Типичные ошибки кандидатов
- Чтение с реплики сразу после записи без учёта lag: клиент пишет на мастер, а читает с реплики и не видит своего же изменения.
Как исправить: в критичных случаях делать read-your-writes: хранить в сессии признак «была запись» и на короткое время читать с мастера. - Использование задержанной реплики для бэкапа без мониторинга lag: при отставании бэкап может не содержать свежих данных.
Как исправить: мониторитьpg_stat_replicationи тревожиться при задержке > N секунд.
Возможные дополнительные вопросы интервьюера
Чем репликация отличается от кластеризации?
Репликация — копирование данных на отдельные экземпляры; кластеризация (shared-disk) — несколько узлов работают с одним и тем же хранилищем.
Как обеспечить консистентность на реплике при длительной задержке?
Никак, кроме перехода на синхронную репликацию или выбор мастера для чтения в нужных транзакциях.
Что такое CAP теорема?
Краткий ответ (30–60 секунд)
CAP теорема утверждает, что в распределённой системе при возникновении разрыва сети можно обеспечить только два из трёх свойств: Согласованность (Consistency) — все узлы видят одни и те же данные одновременно; Доступность (Availability) — каждый запрос к работающему узлу завершается успешно; Устойчивость к разделению (Partition Tolerance) — система продолжает работать при потере связи между узлами. На практике partition tolerance обязательна, поэтому выбор между CP и AP.
Развёрнутый ответ
Формально CAP сформулирована сетевым разделением, где часть узлов не может общаться. Если выбрать CP (MongoDB, HBase), при разделении недоступная часть теряет доступность (отклоняет запросы). Если AP (Cassandra, DynamoDB), клиенты всегда получают ответ, но данные могут быть несогласованными (eventual consistency).
CAP не говорит, что можно выбрать только два свойства всегда; в отсутствие разделения доступны и C, и A. Это модель для проектирования поведения в наихудшем сценарии.
Под капотом (internals)
- В CP системах алгоритмы консенсуса (Raft/Paxos) выбирают лидера; при разделении узлы без кворума уходят в read-only или недоступны.
- В AP системах используется hinted handoff, read repair для восстановления консистентности после восстановления сети.
- Многие NewSQL базы (CockroachDB, Spanner) пытаются смягчить дихотомию через высокую доступность внутри одного датацентра и CP между ДЦ.
Типичные ошибки кандидатов
- Толковать CAP как «всегда только два из трёх»: забывают упомянуть, что разделение сети — обязательное условие; в штатном режиме система может быть и C, и A.
Как исправить: указывать: «в случае partition, система должна выбрать между C и A». - Навешивать ярлыки: «SQL это CP»: не все SQL БД CP; PostgreSQL standalone не распределён, CAP вообще не применим. Распределённые SQL могут быть CP (CockroachDB) или AP.
Как исправить: применять CAP только к распределённым системам и уточнять поведение при сетеразделении.
Возможные дополнительные вопросы интервьюера
Где в CAP находится PostgreSQL?
В классическом смысле он не распределён, CAP не применим; но при streaming репликации с синхронным режимом скорее CP (при разделении мастер может перестать принимать запись), с async — AP.
Почему partition tolerance нельзя пожертвовать?
Потому что в реальной сети сбои неизбежны; если система не P-tolerant, то при любом сбое связи она становится недоступной, что неприемлемо.
Что такое BASE?
Краткий ответ (30–60 секунд)
BASE — альтернативная ACID модель для распределённых систем, означающая: Basically Available (базовая доступность), Soft state (неустойчивое состояние), Eventual consistency (согласованность в конечном счёте). Отказ от строгой согласованности в пользу доступности и масштабируемости. Типична для NoSQL-систем (Cassandra, DynamoDB).
Развёрнутый ответ
BASE принимает, что данные некоторое время могут быть несогласованными, но через какое-то время после прекращения обновлений все реплики сойдутся к одному состоянию. Противопоставляется жёсткой ACID модели. Широко используется в крупных интернет-сервисах, где допустима небольшая задержка консистентности (корзина интернет-магазина, лента новостей).
Пример: обновление аватара пользователя — какое-то время друзья могут видеть старый аватар, но затем все кэши и реплики обновятся.
В Go-приложениях, работающих с AP-хранилищами (Cassandra, ScyllaDB), нужно явно обрабатывать устаревшие данные: повторные чтения, условные обновления (CAS — compare-and-set) для критичных операций.
Под капотом (internals)
- Eventual consistency достигается механизмами: read repair (при чтении обнаруживаются расхождения и исправляются), hinted handoff (при восстановлении узла недостающие записи доставляются), gossip протокол для обмена информацией о состоянии.
- Soft state: нет гарантий, что записанное значение сохранится, если не было подтверждено кворумом; для повышения надёжности используют уровни консистентности QUORUM.
Типичные ошибки кандидатов
- Применять BASE для финансовых операций: недопустимо, т.к. отсутствие мгновенной согласованности приводит к двойному списанию.
Как исправить: для транзакций с деньгами выбирать CP систему с ACID или применять Saga с компенсациями. - Полагать, что eventual consistency означает «когда-нибудь, возможно»: на самом деле строго гарантируется сходимость при отсутствии новых обновлений.
Как исправить: проектировать систему с учётом известных верхних границ времени сходимости (SLA).
Возможные дополнительные вопросы интервьюера
Можно ли достичь сильной согласованности в BASE-системе?
Да, жертвуя доступностью: используя кворумные чтения/записи (R+W > N), которые обеспечивают строгую консистентность ценой потенциальных отказов.
Как BASE соотносится с CAP?
BASE — подход, ориентированный на AP сторону CAP-теоремы.
В чем разница между OLAP и OLTP?
Краткий ответ (30–60 секунд)
OLTP (Online Transaction Processing) — системы обработки транзакций, оптимизированные для большого количества коротких атомарных операций (вставки, обновления заказов). OLAP (Online Analytical Processing) — системы аналитики, ориентированные на сложные запросы, агрегации и чтение больших объёмов исторических данных. Разница в нагрузке, структуре данных и требованиях к времени отклика.
Развёрнутый ответ
| Характеристика | OLTP | OLAP |
|---|---|---|
| Назначение | Оперативная обработка заказов, бронирования | Бизнес-аналитика, отчёты, BI |
| Тип запросов | Простые CRUD, точечные | Сложные SELECT, GROUP BY, оконные функции |
| Строк за транзакцию | Единицы | Миллионы |
| Модель данных | Нормализованная (3NF) | Денормализованная (звезда/снежинка) |
| Индексы | Много индексов для быстрой записи/чтения | Минимум индексов, колоночное хранение |
| Примеры СУБД | PostgreSQL, MySQL, Oracle | ClickHouse, BigQuery, Redshift |
В реальном мире часто используют связку: OLTP (PostgreSQL) для приёма заказов, затем CDC/ETL переливает данные в OLAP (ClickHouse) для аналитики. Понимание различий критично при выборе архитектуры.
Под капотом (internals)
- OLAP движки часто используют колоночное хранение (columnar storage): значения одного столбца лежат последовательно, что даёт отличное сжатие и эффективные агрегации.
- OLTP использует строковое хранение (row-store) и B-tree индексы для быстрой точечной выборки.
- OLAP-запросы ускоряются векторной обработкой (SIMD) и распределённым выполнением (MPP).
Типичные ошибки кандидатов
- Проектировать индексы в OLAP так же, как в OLTP: создавать B-tree на каждой колонке, замедляя загрузку данных и не ускоряя сканирование всего столбца.
Как исправить: использовать колоночные форматы и специализированные индексы (например, minmax). - Пытаться строить сложные отчёты напрямую на OLTP-базе: приводит к блокировкам и деградации сервиса для пользователей.
Как исправить: выделить реплику для отчётов или перелить данные в аналитическую систему.
Возможные дополнительные вопросы интервьюера
Может ли одна СУБД обслуживать оба типа нагрузки?
HTAP (Hybrid Transaction/Analytical Processing) системы, например TiDB, SingleStore, пытаются, но с компромиссами; для серьёзных нагрузок всё же разделяют.
Почему в OLAP используют колоночное хранение?
Потому что аналитические запросы обычно читают лишь несколько колонок из многих, и колоночное хранение минимизирует I/O и позволяет эффективное сжатие (RLE, дельта-кодирование).
Что такое NoSQL?
Краткий ответ (30–60 секунд)
NoSQL (Not Only SQL) — широкий класс нереляционных баз данных, ориентированных на горизонтальное масштабирование, гибкие или бессхемные модели данных и часто жертвующих полной ACID-совместимостью ради производительности и доступности. Основные типы: key-value, документные, колоночные, графовые.
Развёрнутый ответ
Категории:
- Key-value: Redis, DynamoDB. Быстрый доступ по ключу, крайне просто.
- Документные: MongoDB, Couchbase. Хранят JSON/BSON документы, вложенность, частичная схема.
- Колоночные: Cassandra, HBase. Строки разбиты по колонкам, эффективно для временных рядов и агрегаций.
- Графовые: Neo4j, ArangoDB. Узлы и связи, оптимальны для графовых алгоритмов.
NoSQL не отменяет SQL; многие из них добавляют собственные языки запросов (CQL, MQL). Выбор NoSQL оправдан, когда реляционная модель плохо ложится на данные или нужны экстремальные масштабы записи/чтения.
Под капотом (internals)
- Распределённые NoSQL базы (Cassandra) используют consistent hashing для распределения данных, gossip протоколы, анти-энтропийные ремонты.
- Документные БД часто внутри используют BSON и индексы на полях, включая вложенные.
- Key-value — это чаще всего in-memory с персистентностью (snapshot + AOF в Redis).
- Отсутствие жёсткой схемы позволяет быстро менять структуру («schema-on-read»), но валидация ложится на приложение.
Типичные ошибки кандидатов
- «NoSQL значит без SQL» — использовать для всего: берут MongoDB для финансовых транзакций, где нужна строгая целостность.
Как исправить: выбирать хранилище по требованиям: если нужна ACID-транзакции с JOIN — лучше SQL. - Игнорировать вопросы согласованности: полагать, что все NoSQL БД AP, и не настраивать уровни консистентности (например, Cassandra QUORUM).
Как исправить: изучить модель консистентности выбранной БД и задать нужные параметры.
Возможные дополнительные вопросы интервьюера
Почему Redis не подходит для больших объёмов данных, не помещающихся в ОЗУ?
Redis — primarily in-memory, хотя есть виртуальная память и дисковые tiers, но производительность сильно падает; для больших данных лучше использовать SSD-оптимизированные key-value типа RocksDB.
Как в NoSQL решается проблема объединения данных из разных «таблиц»?
Через денормализацию, вложенные документы, или application-side joins (склеиваем в коде).
Чем NoSQL отличается от SQL?
Краткий ответ (30–60 секунд)
SQL базы данных используют реляционную модель, строгую схему и язык SQL; оптимизированы под ACID-транзакции и сложные объединения. NoSQL базы жертвуют реляционной моделью ради горизонтального масштабирования и гибкости схемы, поддерживают различные модели данных (документо-, ключ-значение, графы) и чаще используют BASE-подход.
Развёрнутый ответ
| Критерий | SQL (реляционные) | NoSQL (нереляционные) |
|---|---|---|
| Модель данных | Таблицы, строки, колонки | Документы, key-value, графы, колонки |
| Схема | Жёсткая, схема-first | Гибкая, schema-on-read |
| Масштабирование | Вертикальное + репликация (читающих) | Горизонтальное, шардирование из коробки |
| Транзакции | ACID (строгие) | Чаще BASE, некоторые поддерживают ACID |
| Язык запросов | SQL стандартизован | Специфичные диалекты, часто NoSQL-запросы |
| Сообщество | Зрелое, много инструментов | Разнообразное, разные экосистемы |
Тенденция: NewSQL (CockroachDB, Spanner) — распределённые SQL базы с горизонтальным масштабированием и ACID, стирают разницу.
Под капотом (internals)
- SQL: B-tree, MVCC, оптимизатор на основе стоимости.
- NoSQL: LSM-деревья (Cassandra, RocksDB), DHT-кольца (Dynamo), gossip.
- Многие NoSQL хранилища используют append-only модели без блокировок.
Типичные ошибки кандидатов
- Считать, что NoSQL быстрее SQL по определению: на точечных запросах хорошо спроектированный SQL с индексами часто быстрее, потому что у NoSQL может не быть сложного оптимизатора.
Как исправить: сравнивать бенчмарки на своём паттерне нагрузки. - Полагать, что в NoSQL нет транзакций вообще: многие (MongoDB 4.0+, DynamoDB, CosmosDB) поддерживают ACID-транзакции в рамках шарда или распределённые.
Как исправить: изучать документацию конкретной БД.
Возможные дополнительные вопросы интервьюера
Может ли SQL база заменить NoSQL?
Иногда да: PostgreSQL с JSONB колонкой может работать как документное хранилище, обеспечивая и ACID, и гибкую схему.
Что выбрать для логов и аналитики?
Колоночную NoSQL (ClickHouse, Cassandra) либо SQL с колоночными расширениями.
Что такое инвертированный индекс?
Краткий ответ (30–60 секунд)
Инвертированный индекс — структура данных, отображающая каждый уникальный термин (слово) в список идентификаторов документов (или позиций), в которых этот термин встречается. Ключевой компонент поисковых движков (Elasticsearch, Solr, PostgreSQL GIN), обеспечивающий быстрый полнотекстовый поиск.
Развёрнутый ответ
В отличие от B-tree, где ключ → значение строки, здесь слово → список документов. Пример:
“hello” → [doc1, doc23]
“world” → [doc1, doc15]
Запрос hello AND world даст пересечение списков (doc1). Дополнительно хранят частоту термина, позиции для релевантности и фразового поиска.
PostgreSQL использует GIN-индекс (Generalized Inverted Index) для полнотекстового поиска, массивов, JSONB. GIN содержит:
- Словарь токенов (entries)
- Дерево для поиска токена
- Posting list (TID-ы строк)
В Go-приложениях инвертированные индексы чаще всего используются через Elasticsearch или встроенную возможность PostgreSQL.
Под капотом (internals)
- GIN-индекс в PostgreSQL — это B-tree по элементам (токенам), где каждый элемент ссылается на несортированный список TID строк (posting list). Для ускорения сжатия списки хранятся в виде compressed bitmap.
- При вставке новой строки нужно обновить все posting lists, что может быть дорого; поэтому GIN поддерживает fastupdate — отложенную вставку в отдельный несортированный список, который периодически сливается с основным.
Типичные ошибки кандидатов
- Использовать GIN для столбцов с высокой кардинальностью уникальных значений: каждое значение встречается один раз — вырождается в обычный B-tree, но медленнее.
Как исправить: для уникальных полей — классический B-tree. - Игнорировать параметры
fastupdate: при интенсивной вставке данных без fastupdate производительность сильно падает.
Как исправить: включатьfastupdateдля часто изменяемых таблиц.
Возможные дополнительные вопросы интервьюера
Чем поиск в Elasticsearch отличается от PostgreSQL GIN?
Elasticsearch построен на библиотеке Lucene, хранит сегменты и оптимизирован для поиска по большим объёмам, поддерживает распределённый поиск; GIN — встроенный индекс транзакционной БД.
Можно ли использовать GIN для тегов?
Да, массив тегов с GIN индексом позволяет быстро находить все записи с заданным тегом.
Что такое блокировки (locks) и латчи (latches)?
Краткий ответ (30–60 секунд)
Блокировки (locks) — механизм управления конкурентным доступом на логическом уровне (строки, таблицы), защищающий данные от несогласованных изменений; удерживаются на протяжении транзакции. Латчи (latches) — низкоуровневые примитивы синхронизации (мьютексы, спинлоки), защищающие внутренние структуры данных в памяти (страницы буферного пула) на очень короткое время (микросекунды). Locks — про целостность данных, latches — про целостность внутреннего состояния СУБД.
Развёрнутый ответ
- Locks: режимы: Shared (S, для чтения), Exclusive (X, для записи), Update (U) и т.д. Подчиняются двухфазной блокировке (2PL) — фаза наращивания и фаза снятия в конце транзакции. Могут быть автоматическими (по SQL) или явными (
SELECT FOR UPDATE). DBA видит их черезpg_locks. - Latches: спинлоки, RW-locks, используемые при доступе к странице в буферном пуле или при изменении списка свободных буферов. Они не отражаются в системных каталогах, но косвенно видны по ожиданиям (
pg_stat_activitywait events). В Go аналог —sync.Mutex, но на уровне базы.
Взаимодействие: перед чтением строки на странице берётся latch на страницу, затем проверяется lock на строку. Разделение уровней позволяет избежать дорогих проверок блокировок при каждом физическом доступе.
Под капотом (internals)
- В PostgreSQL latch реализован как
spinlockиlwlock. Для страниц используетсяLWLockв эксклюзивном (при записи) или разделяемом (при чтении) режиме. - Locks управляются хеш-таблицей, XID транзакции и объектом блокировки. Deadlock detector ищет циклы и отменяет транзакцию.
- В InnoDB — мьютексы и rw-locks; блокировки строк — лочатся через lock table в памяти.
Типичные ошибки кандидатов
- Смешивать понятия: «блокировка строки — это мьютекс» — нет, это логический объект, существующий дольше и управляемый менеджером транзакций.
Как исправить: запомнить: locks — защита данных, latches — защита метаданных. - Игнорировать latch contention при анализе производительности: если много процессов ждут
LWLock, это видно в wait events, и проблема в горячих страницах.
Как исправить: мониторитьpg_stat_activity.wait_event_type = 'LWLock'и искать корень (например, высокая конкуренция на чтение).
Возможные дополнительные вопросы интервьюера
Почему latch нельзя удерживать длительное время?
Потому что он блокирует доступ к физической странице всем процессам, парализуя систему; поэтому реализован как лёгкий примитив с быстрым захватом.
Как latch связан с атомарностью WAL-записей?
При вставке WAL-записи берётся latch на соответствующий буфер WAL, чтобы запись была атомарной.
Что такое дедлок в базах данных?
Краткий ответ (30–60 секунд)
Дедлок (deadlock) — ситуация, когда две или более транзакции взаимно блокируют друг друга, ожидая освобождения ресурсов, захваченных другой стороной. В результате ни одна не может продолжиться. СУБД обнаруживает дедлок и аварийно завершает одну из транзакций (жертву), откатывая её, чтобы разблокировать другие.
Развёрнутый ответ
Пример: Транзакция A захватывает строку X и хочет строку Y; транзакция B захватила Y и хочет X. Каждая ждёт снятия блокировки, которой не будет.
СУБД не предотвращает дедлоки (разве что при использования таймаутов), а выявляет по графу ожиданий (waits-for graph). Как только найден цикл, выбирает жертву обычно по критерию «наименьший объём работы» (минимальное число изменений в WAL). Приложение должно обрабатывать ошибку сериализации (deadlock detected, SQLSTATE 40P01) и повторять транзакцию.
В Go: tx.Exec(...) возвращает ошибку, которую можно проверить:
if pqErr, ok := err.(*pq.Error); ok && pqErr.Code == "40P01" {
// retry logic
}
Под капотом (internals)
- PostgreSQL: deadlock detector запускается периодически (deadlock_timeout). Проверяет граф блокировок. Может быть настроен через
log_lock_waits. - InnoDB: использует алгоритм обнаружения циклов, жертву выбирает по весу (количество изменённых строк).
- Жертва: откатывается, её блокировки снимаются.
Типичные ошибки кандидатов
- Игнорировать дедлоки, считая их редкими: в системах с высоким параллелизмом они случаются регулярно; код обязан предусматривать повтор.
Как исправить: реализовать retry-логику с экспоненциальной задержкой и ограничением числа попыток. - Пытаться предотвратить дедлоки полным упорядочиванием доступов (lock ordering): в сложных ORM это практически невозможно, проще полагаться на детектирование и повтор.
Как исправить: придерживаться единого порядка захвата ресурсов, насколько это возможно, и всё равно обрабатывать deadlock.
Возможные дополнительные вопросы интервьюера
Может ли SELECT вызывать дедлок?
Да, если используется SELECT FOR UPDATE запрос с блокировками, даже обычный SELECT в Serializable транзакции может стать жертвой из-за конфликтов сериализации.
Что делать, если дедлоки происходят слишком часто?
Анализировать логи, пересмотреть логику захвата блокировок, возможно, уменьшить время транзакции, понизить уровень изоляции.
Как работают триггеры и когда их использовать?
Краткий ответ (30–60 секунд)
Триггер — предварительно определённая процедура, автоматически запускаемая при выполнении определённого события (INSERT, UPDATE, DELETE) над таблицей. Используются для поддержания сложных ограничений целостности, аудита изменений, автоматического обновления производных данных. В современной архитектуре с микросервисами их стоит применять осмотрительно, чтобы не скрывать бизнес-логику.
Развёрнутый ответ
Триггеры бывают:
- BEFORE / AFTER на событие,
- INSTEAD OF для представлений,
- Per statement или per row.
Пример аудита изменений:
CREATE TRIGGER audit_changes AFTER UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION log_change();
В Go приложениях часто предпочитают реализовывать аналогичную логику на уровне приложения или через CDC, чтобы не завязываться на процедурный SQL, но триггеры эффективны для гарантированной регистрации всех изменений.
Под капотом (internals)
- Триггерные функции выполняются в контексте транзакции, вызвавшей событие. Если триггер кидает ошибку, триггерное действие и сама операция откатываются.
- В PostgreSQL триггеры реализованы как механизм вызова функций на C/PL/pgSQL. Транзакция обрабатывает триггеры синхронно, что может замедлить основную операцию.
- При триггере на каждую строку (
FOR EACH ROW) происходит переключение контекста для каждой изменённой строки, что дороже, чемFOR EACH STATEMENT.
Типичные ошибки кандидатов
- Перегрузка триггерами бизнес-логики: размещение сложных проверок или вызовов внешних API внутри триггера, что приводит к неочевидному поведению и замедляет запись.
Как исправить: выносить бизнес-логику в уровень приложения или в фоновые воркеры, а триггеры оставить только для задач целостности. - Забывать, что триггер вызывается рекурсивно: по умолчанию триггер на UPDATE может сам вызвать UPDATE и бесконечную рекурсию.
Как исправить: управлятьsession_replication_roleили использовать флаги.
.
Возможные дополнительные вопросы интервьюера
Можно ли полностью отказаться от триггеров?
В некоторых архитектурах их заменяют CDC (Debezium + Kafka), перенося реакцию на изменения в стриминговый слой.
Чем триггер отличается от правила (rule) в PostgreSQL?
Правила — это переписывание запроса, работают на уровне планировщика; триггеры — на уровне событий строк. Правила могут быть неинтуитивными.
Что такое хранимые процедуры?
Краткий ответ (30–60 секунд)
Хранимая процедура — именованный блок кода на процедурном языке (PL/pgSQL, PL/Tcl), хранящийся и выполняющийся на сервере БД. Инкапсулирует сложную логику работы с данными, уменьшая сетевые обмены и обеспечивая централизованное управление. Может принимать параметры и возвращать результаты, в том числе наборы строк.
Развёрнутый ответ
Преимущества:
- Эффективное выполнение нескольких SQL операций без лишних round-trips.
- Повторное использование, централизованное обновление логики.
- Снижение нагрузки на клиентский язык.
Недостатки:
- Усложняет отладку, тестирование и версионирование.
- Привязывает логику к конкретной СУБД, усложняя миграцию.
- Может стать «монолитом» в базе данных.
В Go-приложениях часто предпочитают хранить логику в коде на Go и использовать БД только как хранилище, однако для сложных аналитических вычислений или пакетной обработки процедуры могут быть полезны.
Под капотом (internals)
- Хранимая процедура компилируется в байткод или AST. Выполняется в процессе сервера БД с возможностью использования подготовленных планов для операторов.
- Транзакционный контекст: внутри процедуры можно управлять транзакциями (COMMIT/ROLLBACK) начиная с PostgreSQL 11+ через подтранзакции.
- Процедуры могут вызывать другие процедуры, образуя модули.
Типичные ошибки кандидатов
- Создание процедур, выполняющих медленные запросы без анализа плана: план фиксируется при первом вызове и может быть неоптимальным для последующих параметров.
Как исправить: проверять план сauto_explain, использоватьEXECUTE ... USINGдля динамических запросов. - Использование хранимых процедур для бизнес-логики, зависящей от внешних сервисов: вызов HTTP из БД — путь к проблемам стабильности.
Как исправить: оставить в процедурах только манипуляции с данными.
Возможные дополнительные вопросы интервьюера
В чём отличие процедуры от функции в PostgreSQL?
Функция возвращает значение/таблицу и вызывается внутри SELECT; процедура выполняется через CALL и может управлять транзакциями.
Стоит ли мигрировать всю бизнес-логику в хранимые процедуры?
Обычно нет, т.к. это усложняет CI/CD и противоречит микросервисной архитектуре; лучше держать логику в Go.
Что такое "сериализуемость" (serializability)?
Краткий ответ (30–60 секунд)
Сериализуемость — высший уровень изоляции транзакций, гарантирующий, что результат параллельного выполнения группы транзакций эквивалентен некоторому последовательному порядку их выполнения. То есть конкурентно исполняемые транзакции ведут себя так, как если бы они выполнялись одна за другой. Исключает все возможные аномалии: грязные чтения, неповторяющиеся чтения, фантомы и write skew.
Развёрнутый ответ
Сериализуемость может достигаться разными методами:
- Two-Phase Locking (2PL): блокировки удерживаются до конца транзакции; страдает параллелизм.
- Snapshot Isolation (SI): не даёт полной сериализуемости (возможен write skew).
- Serializable Snapshot Isolation (SSI): расширение SI, отслеживающее конфликты и откатывающее транзакции при попытке нарушения.
PostgreSQL реализует SSI с версии 9.1. В Go-коде при выборе уровня Serializable нужно быть готовым к тому, что транзакция может завершиться с ошибкой сериализации (40001) и требовать повтора.
Под капотом (internals)
- SSI в PG использует «serialization graph testing». Когда транзакция читает или пишет, система регистрирует зависимости. При коммите проверяет отсутствие циклов. Если цикл есть — откат с ошибкой сериализации.
- Для этого поддерживаются предикатные блокировки (предикатные SIREAD замки), которые не блокируют, а только записывают факт чтения.
Типичные ошибки кандидатов
- Предполагать, что Repeatable Read равен Serializable: как раз при Repeatable Read возможен write skew; полная сериализуемость требует дополнительных механизмов.
Как исправить: явно указыватьSERIALIZABLE, если бизнес-логика требует полной изоляции. - Не реализовывать повтор при ошибке сериализации: получив
could not serialize access, приложение должно повторить транзакцию целиком.
Как исправить: цикл повтора с экспоненциальной задержкой.
Возможные дополнительные вопросы интервьюера
Всегда ли сериализуемость означает идеальную согласованность?
Да, в рамках транзакций базы данных она гарантирует отсутствие любых аномалий, но не покрывает согласованность на уровне бизнес-правил вне БД.
Как сериализуемость влияет на производительность?
SSI имеет накладные расходы на отслеживание зависимостей и больше откатов, но при умеренных конфликтах всё ещё приемлемо.
Что такое «фантомное чтение» (phantom read)?
Краткий ответ (30–60 секунд)
Фантомное чтение — аномалия, при которой транзакция выполняет один и тот же запрос (например, по диапазону) дважды и получает разное множество строк, так как другая транзакция вставила новые строки, удовлетворяющие условию, и зафиксировала их между чтениями. Это нарушает требование повторяемости набора строк.
Развёрнутый ответ
Пример: Транзакция A считает количество сотрудников отдела (5), транзакция B добавляет нового сотрудника в тот же отдел и фиксирует; Транзакция A снова считает и видит 6. Новые строки — фантомы.
Уровни изоляции:
- Read Committed — фантомы возможны.
- Repeatable Read — в классическом SQL возможны фантомы, но PostgreSQL и InnoDB их предотвращают.
- Serializable — гарантированно без фантомов.
В PostgreSQL на уровне Repeatable Read фантомов нет из-за использования snapshot isolation, который замораживает видимый набор строк. В InnoDB предотвращаются за счёт gap lock'ов. В Go-приложении, чтобы избежать фантомов для критических операций, можно использовать FOR UPDATE.
Под капотом (internals)
- В PostgreSQL фантомная защита на RR обеспечивается тем, что каждая транзакция имеет snapshot активных XID и видит только строки, созданные до этого snapshot.
- В InnoDB gap lock блокирует промежуток в индексе, предотвращая вставку новых строк, попадающих в этот диапазон.
Типичные ошибки кандидатов
- Путать фантомное чтение с неповторяющимся: неповторяющееся — изменение существующей строки, фантом — появление новой.
Как исправить: чётко разделять: non-repeatable read касается обновлённых/удалённых строк, phantom read — новых строк. - Полагать, что обычный SELECT без FOR UPDATE предотвращает фантомы на низких уровнях: нет, на Read Committed фантомы возможны всегда.
Как исправить: повышать уровень изоляции или использовать пессимистичные блокировки.
Возможные дополнительные вопросы интервьюера
Почему в Repeatable Read PostgreSQL нет фантомов, а в стандарте SQL они есть?
Потому что PG использует Snapshot Isolation, который сильнее ANSI SQL Repeatable Read.
Как запретить фантомы на уровне приложения без смены изоляции?
Можно использовать SELECT ... FOR UPDATE на диапазоне, что вызовет блокировку строк и промежутков (в InnoDB) или конфликт обновления в PG.
Что такое «неповторяющееся чтение» (non-repeatable read)?
Краткий ответ (30–60 секунд)
Неповторяющееся чтение — аномалия, при которой транзакция дважды читает одну и ту же строку и получает разные значения, потому что другая транзакция изменила и зафиксировала эту строку между чтениями. Возникает при уровнях изоляции ниже Repeatable Read; нарушает целостность логики, построенной на повторном чтении.
Развёрнутый ответ
Пример: Транзакция A читает баланс пользователя (100). Транзакция B списывает 50 и фиксирует. Транзакция A повторно читает и видит 50. Это может сломать бизнес-правила (сравнение до и после).
Чтобы избежать на стандартном SQL, нужен уровень Repeatable Read или выше. В PostgreSQL на уровне Read Committed так и происходит; для защиты от non-repeatable read можно использовать SELECT FOR UPDATE, которая накладывает блокировку, не дающую изменить строку до коммита.
Под капотом (internals)
- В MVCC при Read Committed каждый оператор получает новый snapshot; при RR snapshot берётся один раз на начало транзакции.
- InnoDB на RR уровне предотвращает non-repeatable reads за счёт блокировок строк при чтении с
FOR UPDATEили за счёт согласованного чтения (undo).
Типичные ошибки кандидатов
- Считать, что неповторяющееся чтение — это то же, что грязное: грязное чтение видит незафиксированные изменения, неповторяющееся — уже зафиксированные другой транзакцией.
Как исправить: уточнять, что оба чтения видят зафиксированные данные, но разные версии. - Полагать, что ORM защищает от неповторяющегося чтения без настроек: ORM просто выполняет SQL; уровень изоляции задаётся в конфигурации БД.
Как исправить: настраивать уровень изоляции и понимать последствия.
Возможные дополнительные вопросы интервьюера
Как избежать неповторяющегося чтения, не меняя уровень изоляции?
Использовать SELECT FOR UPDATE или SELECT FOR SHARE, чтобы заблокировать нужные строки от изменений.
Влияет ли неповторяющееся чтение на целостность foreign key?
Если FK проверяется при разных чтениях, может возникнуть несогласованность, но обычно проверки происходят в момент DML, а не при повторном чтении.
Что такое «грязное чтение» (dirty read)?
Краткий ответ (30–60 секунд)
Грязное чтение — аномалия, при которой транзакция читает незафиксированные (dirty) данные, модифицированные другой параллельной транзакцией. Если вторая транзакция откатится, первая работает с данными, которые никогда не существовали с точки зрения согласованного состояния. Допускается только на уровне Read Uncommitted, не используется в PostgreSQL совсем.
Развёрнутый ответ
Пример: Транзакция A переводит деньги (balance = -100) и ещё не зафиксировалась; транзакция B читает этот отрицательный баланс, принимает решение (отказ в кредите). Потом A откатывается.
PostgreSQL предотвращает dirty read даже на Read Uncommitted уровне, трактуя его как Read Committed. MySQL InnoDB также блокирует dirty reads на всех стандартных уровнях. В реальных приложениях грязные чтения почти никогда не используются осознанно из-за опасности.
Под капотом (internals)
- MVCC автоматически избегает dirty reads: транзакция видит только строки, созданные транзакциями с XID до её snapshot и зафиксированными.
- При использовании блокировок грязное чтение означает чтение без какой-либо блокировки незафиксированной версии, что требует специального режима (в PostgreSQL не поддерживается).
Типичные ошибки кандидатов
- Утверждать, что Read Uncommitted в PostgreSQL допускает грязные чтения: это не так; для совместимости Read Uncommitted ведёт себя как Read Committed.
Как исправить: изучить документацию конкретной СУБД. - Использовать Read Uncommitted для увеличения производительности, надеясь на отсутствие блокировок: в PostgreSQL он не даст выигрыша, а в SQL Server может привести к неконсистентности.
Как исправить: пересмотреть необходимость такого риска; обычно достаточно Read Committed.
Возможные дополнительные вопросы интервьюера
Приведите реальный случай, когда грязное чтение было бы полезно.
Разве что для отладочного мониторинга незафиксированных изменений при анализе долгих транзакций, но даже тогда есть pg_stat_activity и другие средства.
Как PostgreSQL обеспечивает отсутствие dirty read?
Через MVCC: каждая версия строки имеет xmin/xmax и видимость определяется по метаданным коммита (hint bits), незакоммиченные версии не видны.
Что такое «читатель-писатель» блокировка?
Краткий ответ (30–60 секунд)
Читатель-писатель блокировка (RW lock) — примитив синхронизации, разрешающий либо множественный одновременный доступ для чтения, либо исключительный доступ для записи, но не одновременно. В базах данных это latch на уровне страниц или защелки на внутренние структуры, а также блокировки строк в режиме Shared (S) и Exclusive (X).
Развёрнутый ответ
В контексте СУБД RW-блокировки применяются на двух уровнях:
- Logical locks: Shared lock (S) позволяет другим читателям тоже получить S, но блокирует Exclusive (X). X lock блокирует всех. Это стандартный режим совместимости для DML.
- Latches: LWLock в PostgreSQL — вариант RW lock, используемый при чтении/записи страницы буфера.
Основная цель — обеспечить конвейерную обработку чтений, не блокируя друг друга, но гарантируя, что запись видит консистентное состояние.
Под капотом (internals)
- PostgreSQL использует легковесные
LWLockс режимами:LW_SHARED(чтение) иLW_EXCLUSIVE(запись). Внутренне используются атомарные операции и очереди ожидающих. - Для блокировок строк используется таблица блокировок с режимами
AccessShareLock,RowExclusiveLockи т.д.
Типичные ошибки кандидатов
- Путать RW-lock с mutex: мьютекс только эксклюзивен; RW-lock допускает параллельное чтение.
Как исправить: помнить, что RW-lock увеличивает параллелизм читателей. - Думать, что RW-блокировки всегда лучше мьютексов: при очень коротких критических секциях накладные расходы на управление режимами могут превысить выгоду.
Как исправить: выбирать примитив в зависимости от времени удержания и соотношения читателей/писателей.
Возможные дополнительные вопросы интервьюера
Как RW-блокировки предотвращают голодание писателей?
Реализации могут повышать приоритет запросов на запись или запрещать новые разделяемые захваты, если есть ожидающий эксклюзивный.
В чём отличие RW-блокировки от RCU (Read-Copy-Update)?
RCU позволяет читателям вообще не брать блокировку, а писатели создают новую версию, что сложнее, но даёт максимальный параллелизм.
Что такое "журнал предзаписи" (WAL)?
Краткий ответ (30–60 секунд)
WAL (Write-Ahead Log) — журнал, в который база данных записывает все изменения до их применения к основным файлам данных. Это гарантирует долговечность и атомарность: даже при внезапном сбое после перезагрузки СУБД восстановит согласованное состояние, воспроизведя записи журнала. WAL также используется для репликации и инкрементальных бэкапов.
Развёрнутый ответ
Принцип: сначала изменение записывается в WAL, затем, когда-нибудь позже, модифицируется страница данных в буферном кеше. Если крах случится до сброса страницы, при восстановлении WAL проигрывается заново (redo). WAL-записи содержат всю необходимую информацию для повторения операции. В PostgreSQL WAL — это непрерывный поток записей в pg_wal, каждая имеет LSN (Log Sequence Number).
Для Go-разработчика важно: параметры синхронности WAL (synchronous_commit) влияют на задержку и надёжность. При synchronous_commit = off транзакция не ждёт сброса WAL на диск, что быстрее, но при сбое может потерять последние транзакции.
Под капотом (internals)
- WAL-записи формируются в буфере WAL (размер
wal_buffers) и сбрасываются на диск либо при коммите, либо при заполнении буфера. - Каждая запись имеет заголовок с LSN, XID, типом ресурса. Для recovery WAL проигрывается последовательно от последней контрольной точки.
- InnoDB использует redo log, который также является WAL.
Типичные ошибки кандидатов
- Считать WAL только журналом для восстановления: забывать, что он также движок репликации, основа для PITR.
Как исправить: рассказать про потоковую репликацию и архивирование WAL. - Настраивать
synchronous_commit = offдля всех транзакций, считая, что ACID не пострадает: в результате возможна потеря подтверждённых данных при сбое; это нарушает долговечность.
Как исправить: использовать только для некритичных данных или временных таблиц.
Возможные дополнительные вопросы интервьюера
Как WAL связан с контрольными точками?
Контрольная точка — момент, когда все грязные страницы сброшены; все WAL-записи до неё можно удалить (если не нужны для репликации).
Можно ли отключить WAL?
В некоторых СУБД для начальной загрузки данных можно временно (например, в PostgreSQL wal_level = minimal + TRUNCATE + COPY, но это снижает защиту).
Как работает восстановление после сбоев?
Краткий ответ (30–60 секунд)
Восстановление базируется на WAL: при старте СУБД после сбоя она определяет последнюю контрольную точку и проигрывает (redo) все подтверждённые изменения из WAL, начиная с неё. Затем откатывает (undo) незафиксированные транзакции, которые не содержат COMMIT запись в WAL. В результате база приходит в согласованное состояние на момент сбоя.
Развёрнутый ответ
Процесс (на примере PostgreSQL):
- Находит последнюю контрольную точку (из
pg_control). - Начинает REDO с LSN контрольной точки: проходит WAL, для каждой записи применяет изменение к страницам (если это необходимо).
- Достигнув конца WAL, отмечает, что база в согласованном состоянии. Затем приступает к откату незавершённых транзакций (те, у кого нет COMMIT).
- После этого открывается для новых подключений.
Для крупных баз восстановление после аварийной остановки может занять время. Поэтому важно правильно настроить checkpoint_timeout и max_wal_size, чтобы не допускать слишком длинный WAL от контрольной точки.
Под капотом (internals)
- PostgreSQL:
xlogвыполняет redo; утилитаpg_resetwalможет сбросить WAL в экстренных случаях (с потерей данных). - InnoDB: redo log применяется при восстановлении; undo log используется для отката.
- Crash recovery обычно автоматический; время восстановления зависит от объёма WAL и производительности дисков.
Типичные ошибки кандидатов
- Думать, что WAL хранит изменения строк в виде SQL: нет, это физическое представление изменений страниц (или логическое в logical replication).
Как исправить: понимать, что WAL низкоуровневый, отсюда быстрота. - Не учитывать
fsync: если ОС кэширует запись WAL, а БД думает, что сбросила, при сбое питания можно получить испорченный WAL.
Как исправить: всегда держатьfsync = onна production.
Возможные дополнительные вопросы интервьюера
Как ускорить восстановление?
Увеличить checkpoint_timeout осторожно, чтобы уменьшить частоту контрольных точек, но не допустить слишком большой WAL; использовать быстрые SSD.
Что такое PITR (Point-in-Time Recovery)?
Восстановление на заданный момент времени с помощью архива WAL + базового бэкапа; непрерывное архивирование WAL.
Что такое "консенсус" (Paxos/Raft)?
Краткий ответ (30–60 секунд)
Консенсус — алгоритмический способ достижения согласия между распределёнными узлами по поводу единственного значения или порядка операций, даже при сбоях части узлов. Paxos и Raft — два известных протокола, применяемые в NewSQL и распределённых системах (etcd, CockroachDB, Spanner) для выборов лидера, репликации журнала и обеспечения linearizability.
Развёрнутый ответ
Raft проще для понимания: узлы начинают выборы лидера, лидер принимает записи от клиентов, реплицирует их на большинство узлов, и по достижении кворума фиксирует. В случае отказа лидера новый лидер избирается. Обеспечивает сильную согласованность (CP система).
Paxos сложнее, лежит в основе Google Spanner (TrueTime) и многих систем. Реализует согласие на одно значение; для последовательности используется Multi-Paxos.
Консенсус используется в Go-экосистеме через библиотеку Raft (hashicorp/raft) для встроенных распределённых хранилищ или управления конфигурациями.
Под капотом (internals)
- Raft разбит на подзадачи: лидер election (рандомизированные таймауты), log replication (AppendEntries RPC), безопасность (никогда не перезаписывать закоммиченные записи).
- В CockroachDB каждая range использует Raft для репликации данных.
- В Paxos: proposers, acceptors, learners; фазы promise и accept.
Типичные ошибки кандидатов
- «Консенсус гарантирует, что все узлы одновременно получают данные»: нет, консенсус гарантирует согласие на порядок операций, а не мгновенную доставку.
Как исправить: чётко разделять консенсус и репликацию данных; консенсус часто используется для реплицированного журнала. - Считать, что протокол консенсуса решает все проблемы CAP: он решает проблему consistency при разделении, но жертвует availability (если нет кворума, система не принимает запись).
Как исправить: указывать, что Raft/Paxos — CP алгоритмы.
Возможные дополнительные вопросы интервьюера
Чем Raft лучше Paxos?
Понятнее, легче реализовать; функционально эквивалентен. В реальных системах чаще используют Raft.
Как консенсус используется в лидерстве?
Выборы лидера через Raft гарантируют, что в любой момент есть не более одного лидера, принимающего записи.
Что такое "двухфазный коммит" (2PC)?
Краткий ответ (30–60 секунд)
Двухфазный коммит — протокол для атомарного выполнения транзакции, охватывающей несколько независимых ресурсов (распределённые БД). Координатор опрашивает участников (prepare), собирает голоса, и если все готовы — фиксирует (commit), иначе откат (abort). Гарантирует ACID в распределённой среде ценой блокировок и вероятности зависания при отказе координатора.
Развёрнутый ответ
Фазы:
- Prepare: координатор отправляет prepare каждому участнику. Участник выполняет транзакцию, гарантируя возможность коммита или отката, и отвечает «готов» или «не готов».
- Commit/Abort: если все «готовы», координатор рассылает commit; если хоть один «не готов» или таймаут — abort.
Уязвимое место: если координатор умирает после отправки prepare, участники блокируют ресурсы, не зная решения. Для решения используют 3PC (дополнительная фаза) или Presumed Abort/Presumed Commit протоколы. В реальных системах 2PC часто заменяют Sagas из-за блокировок.
Под капотом (internals)
- В PostgreSQL 2PC реализован через
PREPARE TRANSACTION 'txid'/COMMIT PREPARED. Поддерживается в пуле соединений и в распределённых транзакциях (через менеджер транзакций). - WAL пишет маркеры prepared транзакции; при восстановлении координатор должен разрешить судьбу таких транзакций.
Типичные ошибки кандидатов
- Не учитывать проблему «неопределённого состояния»: после таймаута не знаешь, закоммитилась ли транзакция у других; повторный запрос может вызвать дублирование.
Как исправить: использовать идемпотентные операции или протоколы с явным разрешением. - Считать, что 2PC не блокирует: на самом деле во время prepare ресурсы заблокированы до финального решения.
Как исправить: минимизировать длительность фазы prepare, применять таймауты.
Возможные дополнительные вопросы интервьюера
В чём разница между 2PC и Saga?
Saga не блокирует ресурсы надолго, используя компенсационные транзакции; 2PC — строгая атомарность ценой блокировок.
Когда стоит применять 2PC в Go-приложении?
При необходимости атомарного изменения в двух разных БД (например, PostgreSQL + MongoDB), обычно через менеджер распределённых транзакций.
Что такое Saga паттерн?
Краткий ответ (30–60 секунд)
Saga — паттерн управления долгоидущими транзакциями в распределённых системах, где глобальная транзакция разбивается на последовательность локальных транзакций, каждая из которых имеет компенсационную транзакцию. Если один из шагов fails, запускаются компенсации уже успешных шагов, возвращая систему в согласованное состояние без долгих блокировок.
Развёрнутый ответ
Два типа координации:
- Choreography (хореография): каждый сервис после своего действия публикует событие; следующий сервис подписывается и выполняет следующий шаг.
- Orchestration (оркестрация): центральный координатор (Saga orchestrator) вызывает сервисы по порядку и при ошибке запускает компенсации.
Saga широко применяется в микросервисах, потому что 2PC блокирует ресурсы и плохо масштабируется. Пример: создание заказа → резервирование товара → списание денег. Если списание не удалось, отменяется резерв и заказ отменяется.
В Go можно реализовать Saga с помощью каналов, воркеров или использовать библиотеки типа Temporal/Cadence, которые управляют компенсациями и восстановлением.
Под капотом (internals)
- Компенсационные транзакции семантически отменяют результат, но не физически возвращают состояние (оставляют след, например, статус «отменён»).
- Оркестратор сохраняет состояние саги, чтобы пережить свои рестарты.
Типичные ошибки кандидатов
- Проектировать компенсации, которые могут не выполниться: например, уведомление по email нельзя «отменить»; нужно отправлять письмо только после успешного завершения саги.
Как исправить: выделять отложенные действия, выполняемые после подтверждения саги. - Не предусматривать идемпотентность шагов: при повторе неудачного шага можно дважды списать деньги.
Как исправить: каждый шаг делать идемпотентным, используя уникальный идентификатор саги.
Возможные дополнительные вопросы интервьюера
Как Sagas гарантируют согласованность при сетевых сбоях?
Saga не гарантирует мгновенную согласованность, только eventual; координатор должен уметь продолжать после сбоя, используя сохранённое состояние.
Чем Sagas отличается от Event Sourcing?
Sagas управляют распределёнными транзакциями с компенсациями; Event Sourcing — хранение всех изменений как событий.
Что такое Eventual Consistency?
Краткий ответ (30–60 секунд)
Eventual Consistency — модель согласованности, при которой гарантируется, что при отсутствии новых обновлений все реплики данных в конечном счёте (со временем) сойдутся к одинаковому состоянию. Пока это не произошло, клиент может читать устаревшие данные. Используется в AP-системах (Cassandra, DynamoDB) для повышения доступности и производительности.
Развёрнутый ответ
Ключевое отличие от strong consistency — допускается «окно несогласованности». В реализации часто применяются:
- Read repair: при чтении обнаруживается расхождение и сразу исправляется.
- Hinted handoff: временное хранение пропущенных обновлений для восстановившегося узла.
- Anti-entropy: фоновый процесс сравнения и синхронизации данных (Merkle деревья в Cassandra).
В Go-приложении важно выбирать уровень консистентности: для критичных операций использовать QUORUM/ALL, что может обеспечить сильную согласованность ценой недоступности при отсутствии кворума.
Под капотом (internals)
- DynamoDB (и Cassandra) позволяют задать N (количество реплик), R (число подтверждений для чтения), W (для записи). При R + W > N получается сильная согласованность; иначе eventual.
- Векторные часы или last-write-wins (LWW) разрешают конфликты при слиянии версий.
Типичные ошибки кандидатов
- Считать, что eventual consistency означает «когда-нибудь, может, данные станут правильными»: на самом деле это строгая гарантия сходимости при условии отсутствия новых обновлений.
Как исправить: подчёркивать детерминированный характер сходимости. - Игнорировать обработку устаревших данных: строить бизнес-логику на предположении, что прочитанное значение всегда актуально.
Как исправить: использовать условные обновления (compare-and-set) или кворумные чтения.
Возможные дополнительные вопросы интервьюера
Как измерить окно несогласованности?
Мониторингом replication lag и количеством несоответствий при read repair; обычно определяется в SLA (например, 1 секунду).
Можно ли на eventual consistency построить строгую сериализуемость?
Нет, для этого нужны консенсус и блокировки, eventual model сама по себе допускает аномалии.
Что такое CDC (Change Data Capture)?
Краткий ответ (30–60 секунд)
CDC — технология захвата изменений данных в источнике (БД) и их доставки во внешние системы в реальном времени. Изменения (INSERT, UPDATE, DELETE) считываются из журнала транзакций (WAL, binlog) без дополнительной нагрузки на основное приложение. Используется для наполнения поисковых индексов, кэшей, аналитических хранилищ, синхронизации микросервисов.
Развёрнутый ответ
Типичная архитектура: PostgreSQL/MySQL → коннектор Debezium → Kafka → консьюмеры в Go (или других сервисах). Консьюмеры получают события и обновляют своё состояние.
Преимущества:
- Минимальная задержка, асинхронность.
- Развязка монолита: аналитика или индексация не влияют на OLTP.
В Go можно писать консьюмеры Kafka с помощью sarama или confluent-kafka-go, разбирая сообщения Debezium (JSON со схемой).
Под капотом (internals)
- PostgreSQL: логическая репликация (протокол репликации и слоты) читает WAL и передаёт изменения через output plugin (pgoutput, wal2json).
- MySQL: binlog в виде row-based репликации.
- Debezium: подключается как реплика, получает поток и преобразует в события.
Типичные ошибки кандидатов
- Игнорирование начальной загрузки (snapshot): CDC даёт только дельту изменений; при новом консьюмере нужно сначала загрузить исходное состояние.
Как исправить: Debezium автоматически делает снапшот, но надо предусмотреть стратегию восстановления после сбоя. - Не обрабатывать tombstone события (удаления): при логической репликации DELETE строка исчезает, потом может прийти tombstone; Elasticsearch должен удалить документ.
Как исправить: парсить тип операции (op: d) и выполнять соответствующие действия.
Возможные дополнительные вопросы интервьюера
Чем CDC лучше триггеров?
CDC не вносит оверхед в OLTP транзакцию, асинхронен и не требует процедурного кода в БД.
Как обеспечить порядок обработки событий CDC?
Порядок гарантируется в рамках одного шарда/партиции Kafka; для глобального порядка нужно использовать один раздел или дополнительный координатор.
Что такое Row-based vs Statement-based репликация?
Краткий ответ (30–60 секунд)
Это форматы передачи изменений в репликации MySQL. Statement-based replication (SBR) — передаются SQL-выражения, выполненные на мастере, и воспроизводятся на реплике. Row-based replication (RBR) — передаются точные изменения строк (какие строки вставлены/удалены/обновлены). RBR точнее, но генерирует больший объём данных; SBR компактнее, но может приводить к недетерминированным результатам.
Развёрнутый ответ
- SBR: проблемы с функциями
NOW(),UUID(),LIMITбезORDER BY— результат на реплике может отличаться. - RBR: воспроизводит фактические изменения, безопасен, но при массовых UPDATE генерирует много записей. MySQL рекомендует mixed mode.
В PostgreSQL физическая репликация — аналог RBR (изменения страниц), логическая — row-based (изменения кортежей). В Go приложении, если вы строите custom CDC, выбор влияет на разбор binlog/WAL.
Под капотом (internals)
- MySQL binlog: содержит события. В SBR — Query_log_event; RBR — Table_map_event + Row_log_event.
- PostgreSQL: physical WAL записи — страничные изменения; logical — сообщения с кортежами.
Типичные ошибки кандидатов
- Предполагать, что statement-based всегда воспроизводится точно: забывают про недетерминированные функции.
Как исправить: для надёжности использовать RBR или mixed, особенно в кластерах. - Игнорировать размер row-based репликации при bulk операциях: ведёт к распуханию binlog и задержкам.
Как исправить: мониторить lag и, возможно, разбивать большие UPDATE.
Возможные дополнительные вопросы интервьюера
Какой формат использует PostgreSQL streaming replication?
Физическая репликация — постраничная, но логическая — row-based.
Можно ли с RBR отследить только изменения определённых колонок?
Да, через filtering на уровне реплики или специальные плагины (в PostgreSQL публикации могут фильтровать колонки).
Что такое "callback hell" в контексте асинхронных операций с БД?
Краткий ответ (30–60 секунд)
Callback hell — ситуация в асинхронном коде, когда многократная вложенность обратных вызовов для последовательных операций с БД делает код нечитаемым и трудно поддерживаемым. В Go это проявляется при злоупотреблении паттерном callback без использования горутин и каналов; с драйверами, поддерживающими синхронный интерфейс database/sql, проблема минимальна, но возможна в event-driven архитектурах.
Развёрнутый ответ
Хотя в Go нет нативных промисов/async-await, callback hell может возникнуть при использовании библиотек с колбеками (например, Redis PubSub или асинхронные драйверы). Однако идиоматичный Go решает это через горутины и каналы: последовательные операции пишутся последовательно внутри горутины, а не через вложенные колбеки.
// Плохо: имитация callback ада
func fetchUser(id int, cb func(User, error)) { ... }
fetchUser(1, func(u User, err error) {
fetchOrders(u.ID, func(orders []Order, err error) { ... })
})
// Хорошо: горутина с последовательным кодом
go func() {
u, err := fetchUser(1)
orders, err := fetchOrders(u.ID)
}()
Callback hell в БД-контексте чаще ассоциируется с Node.js, но стоит упомянуть, что в Go проблема переносится на управление сложными цепочками каналов и горутин, но решается грамотным дизайном.
Под капотом (internals)
- Драйвер
database/sqlпредоставляет синхронные операции, но при использованииgoroutinesс большим количеством асинхронных зависимостей можно получить «горутинный спагетти». - Для сложных пайплайнов данных (чтение из БД, трансформация, запись) лучше использовать паттерн Pipeline с каналами.
Типичные ошибки кандидатов
- Применение callback стиля в Go вместо горутин: Go позволяет последовательный синтаксис внутри горутины, что проще.
Как исправить: использоватьgo func() { ... }для выполнения серии асинхронных запросов. - Неограниченное создание горутин для каждого запроса к БД: это может исчерпать ресурсы БД по соединениям; необходимо вводить пул и rate limiting.
Как исправить: ограничивать количество горутин, работающих с БД, через семафоры (e.g.,golang.org/x/sync/semaphore).
Возможные дополнительные вопросы интервьюера
Как в Go избежать callback hell при работе с несколькими БД?
Использовать errgroup для параллельных запросов и ожидания, и последовательно выполнять код в одной горутине.
Что такое «callback hell» в контексте асинхронных драйверов типа go-redis?
Даже с асинхронными операциями, используя каналы и sync.WaitGroup, можно сохранить линейную читаемость.
Что такое "ORM" и "Raw SQL" – плюсы и минусы?
Краткий ответ (30–60 секунд)
ORM (Object-Relational Mapping) — библиотека, отображающая таблицы БД на структуры языка и генерирующая SQL запросы автоматически (GORM, Ent). Raw SQL — написание запросов вручную средствами драйвера database/sql или pgx. ORM ускоряет прототипирование и CRUD, но может генерировать неэффективные запросы и скрывать логику. Raw SQL даёт полный контроль, производительность и прозрачность, но требует больше шаблонного кода.
Развёрнутый ответ
ORM плюсы:
- Снижение boilerplate кода.
- Встроенные миграции, валидации.
- Защита от SQL-инъекций (через параметризацию).
ORM минусы:
- Проблема N+1 запросов, если не использовать предварительную загрузку.
- Сложность оптимизации сложных запросов (аналитика, оконные функции).
- Скрытая реляционная логика может привести к непредсказуемой загрузке данных.
Raw SQL плюсы:
- Прямое управление запросами, простота анализа EXPLAIN.
- Использование специфичных фич СУБД.
- Более высокая производительность за счёт ручной оптимизации.
В Go экосистеме многие выбирают компромисс: использование библиотек типа sqlx или pgx с небольшими хелперами для сканирования структур, что даёт удобство и контроль. Ent использует кодогенерацию для типобезопасности.
Под капотом (internals)
- GORM на каждый вызов строит цепочку, собирает SQL, использует
database/sql. - При использовании Raw SQL можно применять подготовленные выражения (
Prepare), уменьшая накладные расходы на парсинг. - ORM могут создавать множество отдельных запросов без явного указания
Joins.
Типичные ошибки кандидатов
- Фанатичное использование ORM для любых задач, включая сложную аналитику: ведёт к медленным запросам и невозможности оптимизации.
Как исправить: для сложных выборок использовать сырой SQL сsqlx, а ORM оставить для простых CRUD. - Ручное построение Raw SQL через конкатенацию строк — риск SQL-инъекций.
Как исправить: всегда использовать плейсхолдеры и передавать параметры отдельно.
Возможные дополнительные вопросы интервьюера
Как избежать N+1 в ORM?
Использовать eager loading (Preload/Include) или join-связки.
Что выбрать для high-load сервиса: ORM или Raw?
Обычно Raw с layer-абстракцией (репозиторий), чтобы сохранить производительность и контроль.
Как работают "материализованные представления" (materialized views)?
Краткий ответ (30–60 секунд)
Материализованное представление (matview) — физическая таблица, хранящая результат выполнения запроса. В отличие от обычного view, данные не вычисляются при каждом обращении, а обновляются только при явном обновлении (REFRESH). Это ускоряет чтение ценой хранения и потенциальной несогласованности с исходными данными.
Развёрнутый ответ
Применяется для предварительно вычисленных агрегированных отчётов, денормализованных представлений, кубов данных. В PostgreSQL:
CREATE MATERIALIZED VIEW sales_summary AS
SELECT product_id, SUM(amount) FROM orders GROUP BY product_id;
Затем REFRESH MATERIALIZED VIEW CONCURRENTLY (если есть уникальный индекс) позволяет обновлять без блокировки чтения. Недостаток: ручное обновление или через cron/pg_cron.
Oracle и SQL Server поддерживают автоматическое обновление, PostgreSQL пока нет. В Go-приложении использование matview прозрачно, но нужно планировать стратегию обновления.
Под капотом (internals)
REFRESHбезCONCURRENTLYполностью перестраивает таблицу (быстро, но блокирует чтение).CONCURRENTLYвычисляет разницу и применяет её, но требует дополнительного индекса и медленнее.- При репликации матвью не реплицируется логически как запрос, а её содержимое может синхронизироваться отдельно.
Типичные ошибки кандидатов
- Создавать матвью без уникального индекса и потом пытаться обновить concurrently: получим ошибку; нужно уникальное покрытие.
Как исправить: добавить уникальный индекс по всем колонкам или подмножеству. - Использовать матвью как замену индексу: если запрос не агрегирующий, а просто денормализованный, лучше создать обычные индексы или covering index.
Как исправить: анализировать, действительно ли нужно кэшировать результат.
Возможные дополнительные вопросы интервьюера
Как реализовать автоматическое обновление матвью в PostgreSQL?
Через триггеры, pg_cron или внешние планировщики с проверкой изменений.
В чём разница между матвью и простой таблицей, заполняемой по расписанию?
По сути то же самое, но matview создаётся SQL командой и сохраняет связь с запросом (хотя определение не обновляется автоматически).
Что такое "сегмент" (segment) и "экстент" (extent) в физической организации БД?
Краткий ответ (30–60 секунд)
Это единицы физического хранения в некоторых СУБД (Oracle, SQL Server). Сегмент (segment) — набор экстентов, принадлежащих одному объекту (таблице, индексу). Экстент (extent) — непрерывная группа блоков/страниц на диске (например, 8 страниц). Блок/страница — минимальная единица I/O. Такая иерархия оптимизирует выделение места.
Развёрнутый ответ
В PostgreSQL прямого понятия сегмента/экстента нет — файлы сегментируются по 1 ГБ и состоят из страниц по 8 КБ. В Oracle же таблица сегментируется на экстенты, а внутри экстента — блоки данных. Экстенты могут динамически выделяться; если пространство заканчивается, сегмент расширяется добавлением экстента.
Понимание этой иерархии важно для работы с фрагментацией и настройками хранения. Для Go-разработчика, возможно, не критично, но на уровне архитектора БД нужно.
Под капотом (internals)
- Oracle: data block (обычно 8K), extent (от 1 до многих блоков), segment (таблица, индекс, undo segment). Словарь данных отслеживает свободные экстенты.
- PostgreSQL: каждый объект хранится в одном или нескольких файлах (по 1 ГБ) в
$PGDATA/base/<oid>/. Единица хранения — страница 8К. - Экстенты позволяют читать/записывать большими непрерывными кусками, уменьшая фрагментацию.
Типичные ошибки кандидатов
- Путать сегмент и табличное пространство: сегмент — логическая коллекция экстентов одного объекта, табличное пространство — физическое расположение файлов.
Как исправить: запомнить: табличное пространство -> файлы данных -> сегменты объектов. - Считать, что все БД используют одинаковые единицы: в PostgreSQL нет extent'ов в том же виде; говорить «экстент» уместно только для Oracle/ SQL Server.
Как исправить: уточнять контекст СУБД.
Возможные дополнительные вопросы интервьюера
Как фрагментация на уровне экстентов влияет на производительность?
Выделенные экстенты могут не соседствовать, увеличивая случайный I/O; поэтому производят дефрагментацию (reorg).
Что происходит, когда сегмент не может расшириться?
Ошибка «unable to extend segment» — требуется увеличить табличное пространство или авторасширение файлов.
Что такое "partial index" и "covering index"?
Краткий ответ (30–60 секунд)
Partial index (частичный индекс) — индекс, построенный только для подмножества строк таблицы, определённого условием WHERE. Сокращает размер индекса и ускоряет запросы по этому условию. Covering index (покрывающий индекс) — индекс, включающий все колонки, запрашиваемые в SQL-запросе, позволяя базе не обращаться к основной таблице за дополнительными данными (index-only scan).
Развёрнутый ответ
Partial index:
CREATE INDEX idx_active_orders ON orders (order_date) WHERE status = 'active';
Полезен, когда часто фильтруют именно активные заказы. Индекс меньше, быстрее обновляется.
Covering index:
В PostgreSQL можно включить дополнительные колонки с INCLUDE:
CREATE INDEX idx_covering ON orders (customer_id) INCLUDE (order_date, total);
Тогда запрос SELECT customer_id, order_date, total FROM orders WHERE customer_id = 42 может выполниться как Index Only Scan. До 11 версии покрытие достигалось созданием составного индекса по всем нужным колонкам.
Под капотом (internals)
- Partial index в PG хранит только кортежи, удовлетворяющие
WHERE; планировщик проверяет, можно ли использовать его для запроса. - Index-only scan требует, чтобы видимость строк и версии не требовали обращения к куче (visibility map). VACUUM должен поддерживать актуальность visibility map.
- В InnoDB, если индекс не включает все нужные колонки, всё равно требуется чтение кластеризованного индекса.
Типичные ошибки кандидатов
- Создавать индекс с INCLUDE колонками, которые редко выбираются: увеличивает размер индекса без выгоды.
Как исправить: анализировать частоту запросов и добавлять только часто запрашиваемые колонки. - Полагать, что составной индекс всегда покрывает запросы с префиксом: индекс по (A, B, C) покрывает запрос SELECT A, B FROM t WHERE A=1, но не SELECT B, C ... (при отсутствии A).
Как исправить: проверять порядок колонок и покрытие.
Возможные дополнительные вопросы интервьюера
Чем покрывающий индекс отличается от вторичного индекса в InnoDB?
В InnoDB вторичный индекс автоматически включает первичный ключ; это можно использовать как покрытие для PK-колонок.
Как partial index может помочь в партиционированной таблице?
Если запрос всегда включает ключ партиционирования, partial index может быть ещё меньше.
Как работает "сборка мусора" в MVCC (например, vacuum в PostgreSQL)?
Краткий ответ (30–60 секунд)
Сборка мусора в MVCC — процесс удаления «мёртвых» версий строк, которые больше не видны ни одной транзакции. В PostgreSQL это выполняет команда VACUUM (автоматически autovacuum) и ANALYZE. Она помечает неиспользуемые кортежи как доступные для переиспользования внутри страницы и обновляет карту видимости. Без vacuum таблица раздувается, что замедляет запросы и тратит место.
Развёрнутый ответ
MVCC порождает старые версии при UPDATE и DELETE. VACUUM:
- Сканирует страницы, обнаруживает мёртвые кортежи.
- Помечает пространство как доступное (файл остаётся того же размера, но место внутри страниц переиспользуется).
- Обновляет visibility map (VM), которая ускоряет index-only scans.
VACUUM FULLсжимает таблицу полностью, но блокирует её.
Autovacuum автоматически запускается при накоплении определённого числа изменений. Параметры autovacuum_vacuum_scale_factor и autovacuum_vacuum_threshold регулируют частоту.
Под капотом (internals)
- Процесс autovacuum worker запускается для базы, читает статистику
pg_stat_all_tables. - Внутри vacuum работает как цикл по страницам, сканируя кортежи и сверяя xmin/xmax с глобальным oldest xmin.
- Транзакции, удерживаемые слишком долго, удерживают xmin, мешая удалению даже старых версий (transaction wraparound).
- В InnoDB аналогичную роль выполняет purge thread, удаляя старые версии из undo log.
Типичные ошибки кандидатов
- Отключение autovacuum для «оптимизации»: ведёт к разбуханию и возможному отказу всей базы из-за зацикливания XID (transaction wraparound).
Как исправить: всегда держать autovacuum включённым и следить заage(datfrozenxid). - Игнорирование анализа: без
ANALYZEпланировщик использует устаревшую статистику и выбирает плохие планы.
Как исправить: включить autovacuum analyze или запускать вручную при больших изменениях.
Возможные дополнительные вопросы интервьюера
Что такое transaction ID wraparound failure?
XID счётчик может переполниться, и старые транзакции будут выглядеть как будущие; VACUUM замораживает старые XID, предотвращая это.
Как VACUUM влияет на репликацию?
VACUUM генерирует WAL записи? Нет, обычный VACUUM не пишет много WAL, но VACUUM FULL пишет.
Как устроен "буферный пул" (buffer pool) и зачем он нужен?
Краткий ответ (30–60 секунд)
Буферный пул (buffer cache) — область оперативной памяти, выделенная под кэширование страниц данных и индексов, чтобы минимизировать дисковые операции. Когда СУБД нужна страница, она сначала проверяет буферный пул; при попадании (cache hit) чтение идёт быстро, при промахе (miss) страница загружается с диска. Механизм LRU/clock-sweep управляет вытеснением.
Развёрнутый ответ
В PostgreSQL shared_buffers задаёт размер. Меньше дисковых чтений => выше производительность. Однако слишком большой буфер может вызвать двойное кэширование с ОС, что на некоторых системах вредит. InnoDB использует свой buffer pool с размером innodb_buffer_pool_size.
Стратегии вытеснения:
- PostgreSQL: алгоритм Clock-sweep (приближение LRU). Каждая страница имеет счётчик использования; sweep обнуляет, и если 0 — вытесняется.
- InnoDB: вариант LRU с защитой от сканирования (midpoint insertion).
В Go-приложении настройка буфера влияет на тайминги QueryRow, но напрямую не контролируется.
Под капотом (internals)
- В PG буферный менеджер управляет массивами
BufferDesc. При запросе страницы выполняетсяBufferAlloc: поиск в хеш-таблице, если нет — выбор жертвы, выгрузка при необходимости, загрузка новой. - Грязные страницы сбрасываются background writer'ом или во время контрольной точки.
- Мониторинг:
pg_stat_bgwriter,pg_buffercache.
Типичные ошибки кандидатов
- Выделение очень большого shared_buffers (>40% RAM) без учёта ОС кэша: может привести к свопингу и худшей производительности.
Как исправить: начинать с 25% RAM, тестировать. - Игнорирование двойного буферизации при использовании direct I/O: если БД открывает файлы с O_DIRECT, кэш ОС не используется; иначе ОС кэширует, а shared_buffers тоже, конкурируя за RAM.
Как исправить: согласовывать настройки файловой системы иeffective_cache_size.
Возможные дополнительные вопросы интервьюера
Что такое «buffer hit ratio» и как его измерить?
Процент запросов, обслуженных из буфера без дискового чтения; в PG: blks_hit / (blks_hit + blks_read).
Почему после перезапуска PG общая производительность временно падает?
Буферный пул пуст, идёт прогрев; поэтому используют pg_prewarm.
Что такое "time-series базы данных" и в чем их особенность?
Краткий ответ (30–60 секунд)
Time-series БД (TSDB) оптимизированы для хранения и запросов временны́х меток и связанных с ними значений (метрик, событий IoT, тиков). Их особенности: высокая скорость записи (append-only), эффективное сжатие за счёт последовательности временных рядов, специализированные функции (downsampling, retention policies). Примеры: InfluxDB, TimescaleDB, ClickHouse.
Развёрнутый ответ
Основные паттерны:
- Высокая интенсивность записи (миллионы точек в секунду).
- Запросы по диапазонам времени, агрегации (среднее, max за интервал).
- Массовое удаление старых данных по политике хранения (TTL).
В отличие от OLTP-ориентированных БД, TSDB используют колоночное сжатие (упаковка времени и значений), сегментированное хранение (partition по времени), и часто оптимизированы под SSD. TimescaleDB — расширение PostgreSQL, сочетающее ACID с временными рядами.
Под капотом (internals)
- TimescaleDB: использует гипертаблицы (hypertable), которые автоматически партиционируют данные по времени (+ дополнительный ключ). Сжатие достигается через колоночное сжатие внутри chanks.
- InfluxDB: собственная TSM engine, оптимизированная для временных рядов.
- Prometheus: собственное хранилище, основанное на блоках времени.
Типичные ошибки кандидатов
- Использовать классическую SQL БД для сбора метрик без настройки: приводит к деградации при росте объёма записей и неэффективным запросам.
Как исправить: выбирать TimescaleDB или InfluxDB, либо тщательно партиционировать и настроить индексы. - Не учитывать кардинальность тегов: высокая кардинальность в InfluxDB (миллионы уникальных series) может взорвать память.
Как исправить: ограничивать количество уникальных комбинаций тегов.
Возможные дополнительные вопросы интервьюера
Как TimescaleDB обеспечивает сжатие?
С помощью колоночного сжатия данных внутри чанков, используя алгоритмы типа Gorilla для временных рядов.
Чем Prometheus storage отличается от InfluxDB?
Prometheus — однопоточная запись на локальный диск, ориентирован на pull-модель; InfluxDB — распределённый, push-модель.
Что такое "графовые базы данных" и когда их использовать?
Краткий ответ (30–60 секунд)
Графовые БД хранят данные в виде узлов (вершин) и рёбер (связей) и оптимизированы для обхода графов. Идеальны для сценариев, где отношения между объектами так же важны, как сами объекты: социальные сети, рекомендательные системы, выявление мошенничества, графы знаний. Используют специализированные языки запросов (Cypher, Gremlin).
Развёрнутый ответ
Реляционная БД может моделировать связи через JOIN, но при глубине связей >2-3 это становится неэффективным. Графовые БД (Neo4j) хранят указатели на соседей непосредственно в узлах, обеспечивая константное время обхода (index-free adjacency).
Когда использовать:
- «Друзья ваших друзей» в соцсетях.
- Поиск кратчайшего пути.
- Анализ влияния (PageRank).
В Go можно взаимодействовать через драйверы Neo4j. Альтернатива — PostgreSQL с рекурсивными CTE, но это медленнее на больших глубинах.
Под капотом (internals)
- В Neo4j данные хранятся в виде фиксированных записей в файлах
neostore.nodestore.dbиedgestore. Узел содержит ссылку на первую связь и свойства. - Обход — это перемещение по указателям без хеш-лукапов, что очень быстро.
Типичные ошибки кандидатов
- Применять графовую БД для простых отношений 1:N, легко реализуемых foreign key: излишне усложняет архитектуру.
Как исправить: использовать реляционную БД, пока нет графовых запросов. - Игнорировать проблемы масштабирования: распределённые графовые БД сложны; многие графовые базы плохо масштабируются горизонтально.
Как исправить: оценить требования к объёму данных и при необходимости выбрать распределённое решение (JanusGraph).
Возможные дополнительные вопросы интервьюера
Можно ли реализовать графовые запросы в PostgreSQL?
Да, с рекурсивными CTE (WITH RECURSIVE), но на больших данных может уступать нативной графовой БД.
Что такое LPG (Labeled Property Graph)?
Модель данных, где узлы и связи имеют метки и пары ключ-значение; это основа Neo4j и др.
Что такое "NewSQL" и какие задачи он решает?
Краткий ответ (30–60 секунд)
NewSQL — класс реляционных СУБД, стремящихся объединить ACID-гарантии и SQL-интерфейс классических СУБД с горизонтальной масштабируемостью и отказоустойчивостью NoSQL. Решают проблему масштабирования традиционных SQL без жертвования транзакциями. Примеры: CockroachDB, TiDB, Google Spanner, YugabyteDB.
Развёрнутый ответ
Традиционная SQL база (монолит) не масштабируется горизонтально на запись легко; NoSQL отказывается от ACID. NewSQL предоставляет распределённый SQL с согласованностью (CP/AP в зависимости от настроек). Часто используют:
- Shared-nothing архитектура с шардированием и автоматической ребалансировкой.
- Консенсус (Raft/Paxos) для репликации.
- MVCC и распределённые транзакции.
В Go CockroachDB предоставляет pgwire-совместимость, можно использовать database/sql с драйвером PostgreSQL. Это упрощает переход.
Под капотом (internals)
- CockroachDB организует данные в ranges, реплицируемые через Raft. Транзакции через distributed SQL и optimistic concurrency.
- Google Spanner использует TrueTime для глобально согласованных снапшотов.
- TiDB разделяет вычислительные и хранилищные слои (TiKV — key-value на Raft, TiDB — SQL).
Типичные ошибки кандидатов
- «NewSQL замена всех SQL баз»: подходит не везде; накладные расходы на консенсус могут быть избыточны для простого приложения.
Как исправить: оценивать требования к масштабу; возможно, хватит PostgreSQL с вертикальным масштабированием. - Недооценка задержек в распределённых транзакциях: они выше, чем локальные, из-за сетевых проходов.
Как исправить: минимизировать распределённые транзакции, проектировать ко-локацию данных.
Возможные дополнительные вопросы интервьюера
Чем CockroachDB отличается от Google Spanner?
CockroachDB не требует атомных часов, использует гибридные логические часы; Spanner зависит от TrueTime.
Какие минусы NewSQL?
Больше сложность администрирования, более высокие latencу по сравнению с одиночным экземпляром, ограничения в совместимости с расширениями (например, PostgreSQL-расширения).
Как работает "лимит соединений" и как его правильно настроить?
Краткий ответ (30–60 секунд)
Лимит соединений (max_connections) ограничивает число одновременных коннектов к БД для предотвращения исчерпания ресурсов (память, CPU). Каждый коннект — это процесс (PostgreSQL) или поток (MySQL), занимающий ОЗУ (в PG ~5-10 МБ на коннект). Слишком низкий лимит вызывает очереди и отказы, слишком высокий — деградацию. Рекомендуется дополнять пулом соединений (PgBouncer).
Развёрнутый ответ
Правильная настройка зависит от RAM и нагрузки. Обычная формула для PostgreSQL: оставить ОС и буферному пулу память, остальное поделить на средний размер процесса. Часто оптимально 200-500 коннектов, но не тысячи.
В Go приложении обычно используется пул соединений (SetMaxOpenConns, SetMaxIdleConns). Лучше держать умеренный максимальный размер пула (20-50), а на уровне БД включать pgBouncer в режиме транзакций (transaction pooling), который мультиплексирует множество клиентов на меньшее число реальных соединений.
Под капотом (internals)
- В PostgreSQL каждый подключённый бекенд — отдельный процесс, потребляющий разделяемую память блокировок и кэшей. Высокое количество соединений ведёт к конкуренции за SpinLock, снижению эффективности.
- Connection pooling через pgBouncer использует epoll/kqueue и легковесные структуры.
Типичные ошибки кандидатов
- Устанавливать
max_connections = 1000без учета RAM: приводит к OOM killer и нестабильности.
Как исправить: мониторить использование памяти и снижать до разумного, задействовать пулер. - Открывать соединение на каждый входящий HTTP запрос: быстро исчерпывает лимит БД, так как каждый запрос создаёт новый коннект.
Как исправить: использовать пул соединений в приложении и pgBouncer для мультиплексирования.
Возможные дополнительные вопросы интервьюера
Как pgBouncer в transaction pooling режиме влияет на prepared statements?
Не поддерживаются именованные prepared statements, только протокол extended query; стоит избегать подготовленных выражений на уровне приложения или использовать session pooling.
Что делать, если нужно больше соединений, чем может предоставить БД?
Масштабировать: добавить реплики для чтения, шардирование, использовать асинхронную обработку с очередями.
Что такое "connection pooling" и какие стратегии существуют?
Краткий ответ (30–60 секунд)
Connection pooling — механизм переиспользования открытых соединений к БД вместо открытия нового на каждый запрос. Снижает накладные расходы на TCP handshake, аутентификацию, подготовку бекенда. Стратегии размещения: на стороне приложения (database/sql pool в Go), на стороне прокси (PgBouncer, Pgpool-II), на стороне ORM.
Развёрнутый ответ
Приложение: sql.DB управляет пулом: SetMaxOpenConns, SetMaxIdleConns, SetConnMaxLifetime. Идл-соединения поддерживаются открытыми, чтобы избежать пересоздания.
Прокси-пулер (pgBouncer): сидит между приложением и БД, принимает множество клиентских коннектов и мультиплексирует их на небольшое число серверных соединений.
Режимы работы pgBouncer:
- Session pooling: одно клиентское соединение — одно серверное на всё время сессии; подходит для тяжелых сессионных вещей (PREPARE).
- Transaction pooling: коннект выдается только на время транзакции; после COMMIT/ROLLBACK отпускается для других. Максимально экономит соединения; не совместим с подготовленными выражениями, удерживаемыми через сессию.
- Statement pooling: каждый statement берёт соединение; редко используется.
В Go рекомендовано настраивать пул приложения агрессивно по max idle, ограничить max open, и сверху ставить pgBouncer в transaction режиме для высоконагруженных сервисов.
Под капотом (internals)
database/sqlвнутренне использует очередь ожидания при превышении MaxOpenConns; горутины ждут, пока освободится коннект.- pgBouncer работает на event-driven модели (libevent), использует единый процесс, потребляет мало памяти.
Типичные ошибки кандидатов
- Отсутствие лимита на max open, что может вызвать лавину коннектов: приложение может занять все
max_connections, приведя к отказу других сервисов.
Как исправить: всегда задаватьSetMaxOpenConns. - Неправильное сочетание режима pgBouncer и ORM, которая требует session state (prepared statements, временные таблицы): ломает функциональность.
Как исправить: перевести pgBouncer в session pooling для таких use-case или отказаться от session-зависимых фич.
Возможные дополнительные вопросы интервьюера
Как безопасно закрыть все соединения при graceful shutdown?
Вызвать db.Close() после завершения всех in-flight запросов; использовать контексты с таймаутами.
Что такое коннект-лиз (leaked connections)?
Когда приложение не возвращает коннект в пул (не закрывает sql.Rows или не делает Close у транзакции), пул истощается.
Как работает "failover" и "switchover"?
Краткий ответ (30–60 секунд)
Failover — автоматическое переключение на резервный узел (реплику) при отказе основного. Switchover — запланированное, ручное переключение ролей для обслуживания без потери обслуживания. Оба требуют механизма обнаружения сбоев и выбора нового primary. В PostgreSQL реализуются через Patroni, Stolon, pg_auto_failover.
Развёрнутый ответ
Процесс failover:
- Мониторинг (Patroni, etcd/consul) обнаруживает, что primary недоступен.
- Выбирает наиболее актуальную реплику (наименьший lag).
- Промоутит её до primary (pg_ctl promote или через API Patroni).
- Обновляет DNS/connection string (VIP, HAProxy, pgBouncer).
Switchover инициируется администратором, обычно безопасен: переключение с ожиданием, пока реплика догонит WAL, затем меняет роли.
В Go-приложении при использовании libpq-строки с несколькими хостами (host=primary,replica) драйвер может автоматически переподключаться, но лучше настроить пул соединений с обработкой ошибок и реконнектом.
Под капотом (internals)
- Patroni использует DCS (etcd, ZooKeeper) для лидерских выборов и хранения конфигурации.
- При split-brain возможно появление двух primary; STONITH (fencing) предотвращает это (например, pg_rewind для синхронизации старого primary при возврате).
- VIP (Virtual IP) позволяет абстрагировать адрес.
Типичные ошибки кандидатов
- Полагать, что failover — просто смена IP, без проверки целостности данных: может получиться, что у реплики были отставшие транзакции, которые потеряны.
Как исправить: использовать синхронную репликацию иpg_rewindдля бывшего мастера. - Не тестировать сценарии failover: switchover не проводят регулярно, в результате failover может сломаться при реальной аварии.
Как исправить: проводить регулярные учения, проверять автоматическое восстановление.
Возможные дополнительные вопросы интервьюера
Что такое «split-brain»?
Ситуация, когда две ноды считают себя primary, и оба принимают запись; разруливается fencing (STONITH) или кворумом.
Как обновить пул соединений после failover?
Использовать proxy (pgBouncer с переключением backend), либо перезапускать приложения, либо встроенный retry логики в драйвере.
Как спроектировать схему БД для хранения древовидных данных (adjacency list, nested set, materialized path)?
Краткий ответ (30–60 секунд)
Есть несколько популярных моделей:
- Adjacency List: каждая запись содержит
parent_id. Простая вставка, но сложные запросы всей иерархии (рекурсивные CTE). - Nested Set: левое и правое числа для вложенности. Быстрое получение поддерева, но медленная вставка/обновление.
- Materialized Path: строка-путь типа '1.5.8'. Компромисс: быстрый поиск потомков через LIKE, но отсутствие ссылочной целостности.
Развёрнутый ответ
Adjacency List:
CREATE TABLE categories (
id SERIAL PRIMARY KEY,
parent_id INT REFERENCES categories(id),
name TEXT
);
-- Получить всех потомков:
WITH RECURSIVE tree AS (
SELECT * FROM categories WHERE id = 1
UNION ALL
SELECT c.* FROM categories c JOIN tree t ON c.parent_id = t.id
) SELECT * FROM tree;
Просто для вставки, но recursion каждый раз.
Nested Set (Mighty Oak):
CREATE TABLE categories (
id SERIAL PRIMARY KEY,
lft INT NOT NULL,
rgt INT NOT NULL,
name TEXT
);
-- Потомки: SELECT * FROM categories WHERE lft BETWEEN :parent_lft AND :parent_rgt;
Пересчёт lft/rgt при вставке затратен.
Materialized Path:
CREATE TABLE categories (
id SERIAL PRIMARY KEY,
path LTREE -- или text '1.2.3'
);
-- Потомки: SELECT * FROM categories WHERE path <@ '1'::ltree;
Использует LTREE extension в PostgreSQL, с индексацией GIN.
Под капотом (internals)
- LTREE хранит путь в виде сжатого дерева меток, GiST/GIN индексы ускоряют операции
<@,~и т.д. - Nested Set: обновление требует блокировки таблицы для пересчёта; используют редко.
- PostgreSQL recursive CTE использует work table, итеративный алгоритм.
Типичные ошибки кандидатов
- Выбирать Nested Set для часто изменяемого дерева: каждая вставка вызывает пересчёт половины таблицы.
Как исправить: использовать adjacency list с CTE для запросов, пока глубина не очень велика. - Материализовать путь без LTREE-индексов, делая LIKE запросы:
WHERE path LIKE '1.2.%'— не использует индекс, медленно.
Как исправить: применять LTREE в PostgreSQL или разбивать на массив и индексировать GIN по массиву.
Возможные дополнительные вопросы интервьюера
Как в Adjacency List получить все листовые узлы?
WHERE id NOT IN (SELECT parent_id FROM categories WHERE parent_id IS NOT NULL).
Как обеспечить целостность в Materialized Path?
Через триггеры: при удалении узла каскадно удалять строки с префиксом пути.
Как работает "индекс Брина" (BRIN) и для каких данных он подходит?
Краткий ответ (30–60 секунд)
BRIN (Block Range Index) — индекс, хранящий для каждого диапазона физически последовательных блоков (страниц) обобщённую информацию о значениях колонки (минимум/максимум). Он очень компактный и эффективен для данных, где значения коррелируют с физическим расположением (например, дата добавления в логах, временные ряды). Не подходит для случайно распределённых данных.
Развёрнутый ответ
BRIN создаётся так:
CREATE INDEX idx_brin_date ON orders USING BRIN (order_date);
Вместо точного указателя на каждую строку, он говорит: в диапазоне страниц 0-31 даты между 2020-01-01 и 2020-06-30. Запрос WHERE order_date = '2020-02-01' быстрее, чем Seq Scan, потому что исключаются блоки, в которых точно нет значения. Но чтобы найти точную строку, всё равно придётся просканировать попавший диапазон.
Преимущества:
- Очень маленький размер (сотни килобайт на миллиарды строк).
- Быстрое создание и обновление.
- Идеален для append-only логов, таблиц, отсортированных по времени.
Под капотом (internals)
- BRIN индекс состоит из summary-записей, каждая покрывает
pages_per_rangeблоков. Хранится в B-tree по номеру блока. - При вставке новых данных в пустой блок summary обновляется.
- При запросе планировщик перебирает summary-записи и исключает range'и, где нет перекрытия.
Типичные ошибки кандидатов
- Создание BRIN на колонке с высокой случайностью (UUID): summary не отсеивает почти ничего, и индекс бесполезен.
Как исправить: использовать BRIN только для колонок с естественной корреляцией (дата, последовательный ID). - Слишком большой
pages_per_range: summary слишком грубый, много ложных срабатываний range scan.
Как исправить: подобрать параметр (обычно 32-128), анализируя селективность.
Возможные дополнительные вопросы интервьюера
Можно ли создать составной BRIN индекс?
В PostgreSQL нет; есть расширение btree_gin/btree_gist, но не BRIN. Можно создать несколько BRIN на разные колонки, планировщик может комбинировать.
Как BRIN сравнивается с партиционированием?
Партиционирование даёт pruning, BRIN — тоже исключение диапазонов, но без физического разделения; они могут дополнять друг друга.