Моделирование данных и транзакции
Код на Go редко бывает сложным сам по себе — горутина приняла запрос, сходила в PostgreSQL, отдала ответ. Настоящая сложность сосредоточена в двух местах: как спроектирована схема и как ведут себя транзакции, когда писателей становится много. database/sql и pgx дают прямой доступ к SQL, но не защищают ни от одной концептуальной ошибки — неверная декомпозиция таблиц, неверный суррогатный ключ, неверный уровень изоляции, неверный порядок блокировок: всё это компилируется, проходит локальные тесты и проявляется только под параллельной нагрузкой.
Ловушки тут не про синтаксис. Кандидаты путают нормализацию со сжатием и думают, что высшие нормальные формы объединяют таблицы, а не дробят их. Денормализуют преждевременно и забывают, что цену платит каждая запись. Берут случайный UUID за «просто другой ключ», не замечая, что он ломает append-локальность B-tree. Считают SELECT FOR UPDATE разделяемой блокировкой чтения и ждут, что приложение само должно ловить deadlock. Думают, что Read Committed спасает от фантомов, а read replica масштабирует запись. Эта тема разбирает проектирование и транзакции по слоям — так, чтобы каждый из этих вопросов вы закрывали механизмом, а не заученной фразой.
Карта темы
- Нормализация — 1NF/2NF/3NF, хранение каждого факта ровно один раз и устранение аномалий обновления, вставки и удаления.
- Денормализация — намеренная избыточность ради сокращения join на пути чтения и её цена при каждой записи.
- Ограничения целостности —
NOT NULL,UNIQUE,PRIMARY KEY,FOREIGN KEY,CHECKкак гонко-устойчивые инварианты в самой базе. - Суррогатные ключи — serial и UUID — компактный
serialс append-локальностью вB-treeпротив глобально уникального, но разбросанногоUUID. - Документные vs реляционные базы — гибкая схема и встраивание против join, многострочного ACID и ссылочной целостности.
- OLAP против OLTP — строковое транзакционное хранилище против колоночного аналитического и почему всё решает физическая раскладка на диске.
- Изоляция транзакций — три аномалии, четыре уровня, особенность
Repeatable Readв PostgreSQL и устойчивость через WAL. - Блокировка строк —
SELECT FOR UPDATE, порядок захвата блокировок и почему две транзакции сходятся во взаимоблокировке. - Оптимистичная блокировка — колонка
versionвместо удержания лока, конфликт по числу затронутых строк и повтор. - Масштабирование БД — вертикаль, кеш,
read replica, пул соединений иshardingс их компромиссами по чтению, записи и сложности.
Частые ошибки и ловушки
| Ошибка | Последствие |
|---|---|
| Путать нормализацию со сжатием или ускорением запросов | Неверная ментальная модель — это дисциплина проектирования схемы ради корректности, не storage |
| Думать, что высшие нормальные формы объединяют таблицы | Наоборот — они дробят данные на больше таблиц, убирая дубликаты |
| Денормализовать преждевременно, без профиля чтений | Усложнённая запись и риск рассинхрона без доказанной выгоды |
| Полагаться на валидацию только в коде сервиса | Инвариант обходят ручной SQL, другие сервисы и гонки — UNIQUE в базе единственно гонко-устойчив |
Считать PRIMARY KEY просто UNIQUE | Он ещё NOT NULL и задаёт идентичность строки, на которую ссылаются FOREIGN KEY |
Сказать «UUID медленнее serial», не назвав причину | Причина — потеря append-локальности B-tree из-за случайных вставок и расщеплений страниц, а не «большие байты» |
| Считать документную базу «базой без схемы» | Схема есть, она гибкая на запись; встраивание убирает join, а не добавляет |
Думать, что OLAP — это «OLTP, но больше» | Разница не в размере, а в раскладке: строки против колонок под профиль запроса |
Считать Serializable слабейшим уровнем изоляции | Перевёрнутая модель — Serializable сильнейший, Read Uncommitted слабейший |
Думать, что Read Committed ловит фантомные чтения | На нём возможны и неповторяющиеся, и фантомные чтения |
Считать SELECT FOR UPDATE разделяемой блокировкой чтения | Это эксклюзивная блокировка строки на запись — конкурентные писатели ждут |
| Ждать, что приложение само должно ловить deadlock | Цикл блокировок обнаруживает и рвёт сама СУБД; код лишь повторяет транзакцию-жертву |
| Считать оптимистичную блокировку «локом» в базе | Лока нет вовсе — защита держится на колонке version и проверке числа затронутых строк |
Считать read replica способом масштабировать запись | Реплики снимают только чтения; запись по-прежнему упирается в единственный primary |
Значение для собеседований
Проектирование данных и транзакции — обязательная тема на любом backend-интервью, и спрашивают не «знаешь ли ты слово нормализация», а умеешь ли ты рассуждать о корректности, цене и конкурентности.
Что обычно проверяют:
- Зачем нормализуют схему — устранение избыточности и аномалий, а не скорость запросов; и почему высшие формы дробят таблицы.
- Когда осознанно денормализуют и чем за это платят на стороне записи.
- Почему инвариант ставят в базе (
UNIQUE,FOREIGN KEY,CHECK), а не только в коде сервиса. - В чём разница
serialиUUIDна уровнеB-treeи зачем нуженUUIDv7. - Чем документная база отличается от реляционной и какие гарантии вы теряете.
- Чем колоночное
OLAP-хранилище отличается от строковогоOLTPи почему это про раскладку на диске. - Четыре уровня изоляции, три аномалии и какой уровень что запрещает — и особенность PostgreSQL.
- Что делает
SELECT FOR UPDATE, почему противоположный порядок захвата даёт deadlock и когда вместо лока берут оптимистичную блокировку. - Какими механизмами масштабируют чтения и записи и какова цена каждого.
Типичный неверный ответ: «Read Committed — это безопасный уровень, он защищает от всех аномалий чтения». Это запускает разбор того, что Read Committed запрещает только грязное чтение, а неповторяющиеся и фантомные чтения на нём по-прежнему возможны, и что выбор уровня — это сознательный компромисс между корректностью и конкурентностью.