Базы данных
Реляционная модель vs документные, wide-column, граф и key-value
Стартап принял MongoDB, потому что «реляционные схемы жёсткие». Три года спустя ad-hoc джойны невозможны, целостность данных обеспечивается приложением (непоследовательно), и команда переезжает на Postgres. Жёсткость, которую они хотели избежать, была дисциплиной, которая их бы спасла.
Пространство компромиссов
Реляционная модель — одна из нескольких моделей данных. Опытные инженеры знают, где побеждает каждая альтернатива и где «просто использовать Postgres» превосходит «выбрать специализированный инструмент».
Документные хранилища (MongoDB, Firestore, DynamoDB). Побеждают, когда документы доступны как единицы и никогда не джойнятся. Один документ содержит все данные одной сущности (продукт с вложенными вариантами, медиа и атрибутами). Кросс-документная целостность не нужна. Паттерн запросов — «получить этот документ по ключу» или «фильтр по верхнеуровневым полям». Проигрывают при ad-hoc запросах, операциях эквивалентных JOIN или ссылочной целостности между документами.
Wide-column хранилища (Cassandra, ScyllaDB). Побеждают при очень высокой пропускной способности записи (~100k+ записей/секунду) с заранее определёнными паттернами запросов, не меняющимися после проектирования схемы. Первичный ключ Cassandra включает partition key (как данные распределены) и clustering key (как данные сортируются внутри партиции) — запросы, совпадающие с partition key, быстрые; любой другой запрос — полный скан. Проигрывают при эволюции паттернов запросов или необходимости агрегации.
Граф-базы данных (Neo4j, Dgraph, Amazon Neptune). Побеждают, когда обход графа — доминирующий запрос: «найти всех друзей друзей в 3 переходах», «обнаружить циклические платёжные пути», «найти кратчайший путь в knowledge graph». Проигрывают во всём остальном. Рекурсивные CTE в SQL могут заменить многие граф-запросы на малом масштабе; на большом масштабе или при обходе как основной нагрузке нативная граф-БД дешевле.
Key-value хранилища (Redis, DynamoDB в режиме KV). Побеждают для чистых точечных поисков по известному ключу — хранение сессий, кеши, флаги фич, кратковременные счётчики. Проигрывают при любых диапазонных запросах, агрегации или обходе связей.
| Модель | Побеждает когда | Проигрывает когда | Примеры |
|---|---|---|---|
| Реляционная | Повторяющаяся схема, ad-hoc запросы, кросс-типовая целостность | Чистый документный доступ, only-known-key нагрузки при экстремальном масштабе | Postgres, MySQL |
| Документная | Гетерогенная схема per-row, документный unit-доступ | Ad-hoc запросы, целостность между документами | MongoDB, Firestore |
| Wide-column | Очень высокая пропускная запись, фиксированные паттерны запросов | Эволюция запросов, агрегация | Cassandra, ScyllaDB |
| Граф | Глубокий обход как основной запрос | Всё остальное | Neo4j, Amazon Neptune |
| Key-value | Чистый точечный поиск по известному ключу | Диапазонные запросы, агрегация, обход связей | Redis, DynamoDB (KV mode) |
Postgres — не только реляционная БД
Самоописание Postgres — «самая продвинутая реляционная СУБД с открытым исходным кодом в мире». На практике экосистема расширений покрывает большинство нагрузок, которые можно было бы адресовать специализированному хранилищу:
- JSONB + GIN-индексы — документоподобные запросы без документного хранилища.
- pg_vector — поиск векторного сходства (ближайших соседей для AI-эмбеддингов), заменяет специализированные векторные базы данных для большинства нагрузок.
- PostGIS — полноценный геопространственный движок (пространственные индексы, гео-операторы), заменяет специализированные geo-хранилища.
- TimescaleDB — партиционирование и сжатие временных рядов, заменяет InfluxDB для многих нагрузок.
- pgcrypto — шифрование, хэширование, UUID из криптографического источника.
- Recursive CTE — ограниченный обход графа без граф-базы данных.
Вопрос «нужно ли мне специализированное хранилище?» становится: «превышает ли моя нагрузка то, что может сделать Postgres + нужное расширение?» Для большинства команд до ~10 ТБ и без требований экстремальной пропускной записи ответ — нет.
Postgres vs MySQL: дефолты проектирования схемы, имеющие значение
Обе реляционные; обе реализуют большую часть SQL:2011. Различия, важные при проектировании схемы:
Поведение NULL в UNIQUE constraints. Стандарт SQL (и Postgres) допускает несколько NULL в UNIQUE-колонке (NULL не равен ничему, включая другой NULL). InnoDB MySQL также допускает несколько NULL. Но MySQL исторически (до 8.0) имел другое поведение NULL в некоторых операциях. Используйте Postgres UNIQUE NULLS NOT DISTINCT (SQL:2023), если хотите предотвратить дублирующиеся NULL.
Неявное приведение типов. MySQL более снисходителен — молча преобразует '42' в 42, усекает строки, превышающие длину VARCHAR, вместо ошибки. Postgres строгий — неправильные типы дают явную ошибку. Поведение Postgres выявляет несоответствия схемы раньше.
Дефолтный движок хранения. MySQL по умолчанию использует InnoDB; старый тулинг иногда использует MyISAM (нет транзакций, нет поддержки FK). В 2026 году все продакшн-использования MySQL должны быть на InnoDB. У Postgres один движок хранения без ловушки.
Экосистема. Postgres: PostGIS, TimescaleDB, pg_vector, pgcrypto, citus, pgroll. Экосистема MySQL: Vitess (горизонтальное масштабирование), PlanetScale (MySQL-as-a-service), Aurora MySQL (AWS). Выбирайте MySQL, если глубоко в экосистеме горизонтального масштабирования MySQL (Vitess, PlanetScale, Aurora MySQL); иначе Postgres для нового проекта.
Мультитенантное проектирование схемы: разбор решения
Самое сложное решение схемы для B2B SaaS — изоляция тенантов. Три паттерна:
Schema-per-tenant. Каждый workspace получает собственную Postgres-схему. Полная DDL-изоляция. Миграции выполняются per-schema (n схем × 1 миграция = n операций). Работает до ~1000 тенантов; выше этого накладные расходы на соединения и сложность миграций растут линейно. Переключение тенанта чистое (SET search_path).
Database-per-tenant. Максимальная изоляция. Запретительно сложно выше ~100 тенантов. Подходит для enterprise SaaS с требованиями изоляции по контракту.
Изоляция на уровне строк (колонка workspace_id + RLS). Одна схема, все тенанты в тех же таблицах. Каждая тенантная таблица имеет workspace_id BIGINT NOT NULL. Политики Postgres Row-Level Security (RLS) фильтруют строки на основе session-level GUC:
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
CREATE POLICY projects_tenant_isolation ON projects
USING (workspace_id = current_setting('app.current_workspace_id')::BIGINT);Соединение приложения устанавливает SET app.current_workspace_id = X после аутентификации. RLS гарантирует, что каждый запрос на этом соединении видит только строки workspace X. Пропущенный WHERE в коде приложения поймает RLS.
Эшелонированная защита: RLS + CI-линтер, требующий workspace_id в каждом WHERE. Линтер ловит баги до продакшна; RLS — страховочная сеть.
Почему это работает
Зачем добавлять CI-линтер, если RLS уже применяет изоляцию? RLS — гарантия, а не сообщение об ошибке. Запрос, нарушающий RLS, молча возвращает ноль строк (или выдаёт ошибку, если настроен как RESTRICTIVE). Разработчик видит «нет результатов» и предполагает, что данных нет. CI-линтер проваливает сборку, когда запрос обращается к тенантной таблице без workspace_id, давая actionable ошибку до того, как запрос попадёт в продакшн.
Новая социальная фича нуждается в 'найти всех друзей друзей в 2 переходах' для движка рекомендаций. В таблице users 10M строк и 50M follow-связей. Выберите подход к запросу.
Команда из 200 B2B клиентов рассматривает изоляцию schema-per-tenant. При каком масштабе этот паттерн становится операционно дорогим и почему?
Postgres Row-Level Security включена, политика ограничивает строки `workspace_id = current_setting('app.current_workspace_id')`. Запрос приложения случайно опускает WHERE workspace_id. Что происходит?
- 01Сформулируйте, почему реляционная модель побеждает документные, wide-column, граф и key-value альтернативы для большинства B2B SaaS нагрузок, и дайте одно конкретное условие победы для каждой альтернативы.
- 02Каковы три паттерна мультитенантной изоляции для Postgres и когда применяется каждый?
- 03Назовите два различия в дефолтах проектирования схемы между Postgres и MySQL.
Реляционная модель побеждает для нагрузок с повторяющейся структурой, ad-hoc запросами и критически важной целостностью — большинство B2B SaaS, fintech, операционных инструментов. Документные хранилища побеждают для документного unit-доступа без кросс-документной целостности. Wide-column побеждает при экстремальной пропускной записи с фиксированными запросами. Граф побеждает, когда обход — основной запрос. Key-value побеждает для чистых точечных поисков. Postgres с расширениями (JSONB, pg_vector, PostGIS, TimescaleDB) покрывает большинство специализированных потребностей в одном движке. Для мультитенантного SaaS: изоляция на уровне строк с RLS — по умолчанию; schema-per-tenant для контрактной изоляции до ~1000 тенантов; database-per-tenant для enterprise контрактов. Postgres vs MySQL: Postgres строже (ошибки типов, NULL-семантика) и имеет богатую экосистему расширений; выбирайте MySQL только для специфических потребностей экосистемы или шардинга.
встречается в140
- Почему GraphQL получает N+1junior
- Механика DataLoader: батчинг на границе тикаmiddle
- Контракты batch-функции: порядок, формы, ошибкиmiddle
- Federation и lookahead: батчинг за пределами DataLoadermiddle
- Защита сложности запросов: depth, cost, persisted queriesmiddle
- Senior GraphQL API: scheduling-контракт, изоляция арендаторов, наблюдаемостьsenior
- Зачем идемпотентность: безопасные retryjunior
- Серверный state machine: четыре состояния idempotency keymiddle
- Outbox и inbox: effectively-once через dual-write границуmiddle
- Конкурентность и архитектура кеша для идемпотентности на масштабеsenior
- Наблюдаемость, production-инциденты и дизайн для глобального масштабаsenior
- Event loop: один поток, три очередиjunior
- Задачи, микрозадачи и scheduler.yield()middle
- Голодание микрозадач, длинные задачи и LoAFsenior
- Event loop Node.js: фазы, nextTick и задержка циклаsenior
- React, Vue и наблюдаемость INP в продакшенеsenior
- Render pipeline: шесть стадий от байтов до пикселейjunior
- Цена стадий и модель процесса рендерераmiddle
- Инвалидация, dirty-биты и containmiddle
- Слои композитора: продвижение, перекрытие и память GPUmiddle
- Флейм-стрип DevTools и жизненный цикл кадраmiddle
- Layout thrash: форсированная синхронная компоновкаsenior
- BeginMainFrame, анимации на потоке compositor и память GPUsenior
- Observability в проде: LoAF, INP и полная поверхность атакиsenior
- Что такое V8 и почему производительность различается в 100 разjunior
- Четырёхуровневый JIT-конвейер V8 и профилированная тиеризацияmiddle
- Hidden classes, деревья переходов и расположение в памятиmiddle
- Inline caches, состояния IC и деоптимизацияmiddle
- Orinoco GC: параллельный scavenger, конкурентная разметка и барьеры записиmiddle
- Спекулятивный движок TurboFan и ловушка deopt-loopsenior
- V8 в production: Isolates, сжатие указателей и реальные аварииsenior
- Жизненный цикл service worker и стратегии кешированияmiddle
- Граничные случаи service worker: version skew, долговременность и ловушка навигацииsenior
- Что делает реконсилер: render vs commitjunior
- Объект fiber и дерево с двойной буферизациейmiddle
- Чистота фазы render и подшаги фазы commitmiddle
- Реконсиляция: эвристики диффа и ловушка ключейmiddle
- Приоритетные lanes, time-slicing и useTransitionmiddle
- Bailout, мемоизация и tearingsenior
- React Profiler, компилятор и продакшн-наблюдаемостьsenior
- Стратегии рендеринга: SSG, SSR, ISR, streaming и гидратацияjunior
- SSG, SSR, ISR, streaming и RSC — как работает каждая стратегияmiddle
- Цена гидратации: selective, progressive, острова, resumabilitymiddle
- Hydration mismatch: причины, обнаружение и правило детерминизмаsenior
- RSC, стратегия на маршрут и production-наблюдаемостьsenior
- Core Web Vitals: что измеряют LCP, INP и CLSjunior
- CLS: почему происходят сдвиги лейаута и как их остановитьmiddle
- Трейдоффы метрик, RUM-атрибуция и цикл CI+полеsenior
- Общая картина: от URL до LCP до INP как эстафетаjunior
- Восемь слоёв трассировки: от service worker до второй навигацииmiddle
- Пять канонических поломок: где производство стабильно ломаетсяsenior
- Метод трёх треков: чтение трасс и построение системы мониторингаsenior
- Что такое cache stampede и почему он делает всё хужеjunior
- Лок и single-flight: ограничение параллельных rebuildmiddle
- XFetch: вероятностное раннее истечение без координацииmiddle
- Stale-while-revalidate и CDN request coalescingmiddle
- Детектирование stampede и дизайн TTL для продакшенаmiddle
- Метастабильный сбой, fencing-токены и production-постмортемыsenior
- Роли Raft, term и почему majority-кворум предотвращает split brainjunior
- Как Raft реплицирует log entry и решает, что его безопасно коммититьmiddle
- Выборы лидера в Raft: таймауты, правила голосования и четыре свойства безопасностиmiddle
- Raft в реальном мире: partition, медленный диск и клиентская маршрутизацияmiddle
- Расширения Raft: pre-vote, learner, snapshot и линеаризуемые чтенияsenior
- Raft в production: membership change, Multi-Raft и observabilitysenior
- Где происходит data fetching — и почему это решает LCPjunior
- Fetch waterfall''''ы — диагностика и лечение через Promise.allmiddle
- React Server Components и Suspense streamingmiddle
- Клиентский кэш: TanStack Query, SWR и stale-while-revalidatemiddle
- LCP, prefetch и race conditions в интерактивном fetchingmiddle
- Senior internals: RSC payload, слои кэша и production паденияsenior
- Трёхстороннее рукопожатие TCPjunior
- Номера последовательности и состояние соединенияmiddle
- DNS: что делает и зачем существуетjunior
- Обход резолвера: перенаправления, типы записей и gluemiddle
- TTL, кеширование и распространение DNSmiddle
- Рукопожатие за 1 RTT: key share и ECDHEmiddle
- Возобновление сессии и 0-RTTmiddle
- WebSocket: HTTP-апгрейд до постоянного соединенияjunior
- Формат WebSocket-фрейма: opcodes, маскирование, фрагментацияmiddle
- Backpressure в WebSocket: когда клиенты не успеваютmiddle
- Реконнект: jittered backoff, thundering herd, восстановление сообщенийsenior
- WebSocket в масштабе: HTTP/2 мультиплексирование, permessage-deflate, C10Msenior
- WebSocket в production: прокси, безопасность и распределённая архитектураsenior
- Что делают обратные проксиjunior
- Health checks, connection draining и slow startmiddle
- Session affinity, consistent hashing и правильное решениеmiddle
- Retry-бури, circuit breakers и load sheddingsenior
- Устойчивая архитектура LB: anycast, zone-aware маршрутизация и observabilitysenior
- Почему QUIC, а не TCP+TLSjunior
- Connection ID и миграция сетиmiddle
- Возобновление 0-RTT и шифрование пакетовsenior
- DDoS: что это и почему работаетjunior
- Атаки усиления и истощение состоянияmiddle
- Ограничение скорости: алгоритмы и архитектураmiddle
- WAF, межсетевые экраны, mTLS и HSTSmiddle
- Отравление DNS-кэша и BGP-перехватsenior
- Эшелонированная защита и экономика атакsenior
- DNS, TCP, TLS по очереди: куда уходят миллисекундыmiddle
- Перехват прокси и шлюзы безопасности: rate limiter, WAF, mTLSmiddle
- Альтернативные пути: QUIC 0-RTT, WebSocket upgrade, миграция соединенияmiddle
- Наблюдаемость: распределённые трейсы, USE/RED и семплированиеsenior
- Устойчивость: каскадные повторы, circuit breakers и error budgetsenior
- Что такое три сигнала: метрики, логи, трейсыjunior
- Зачем нужны структурные логи: дневник против таблицыjunior
- Схема продакшн-лога: поля, которые несёт каждая строкаmiddle
- PII-редакция и log injectionsenior
- OTel Logs Data Model и audit-логи как подсистемаsenior
- SLI, SLO и error budget: надёжность в числахjunior
- Error budget policy, latency SLO и составные journeysmiddle
- Продакшн-отказы SLO, самонаблюдаемость, безопасность и общая картинаsenior
- Петля инцидента: от пейджера до постмортема до предотвращенияmiddle
- Cache lines и false sharing: когда параллелизм замедляет кодmiddle
- SIMD и data layout: AoS vs SoA и разница в 4–8xmiddle
- Cache-oblivious алгоритмы, PGO и production failuressenior
- GC в production: наблюдаемость, безопасность, edge cases и управление флотомsenior
- Batching: амортизируй фиксированную цену каждой операцииjunior
- Окно батчинга: размер и время ожиданияmiddle
- Batching в Kafka и Postgresmiddle
- io_uring и наблюдаемость пакетированияmiddle
- От Nagle до io_uring: эволюция пакетированияmiddle
- Backpressure, изоляция сбоев и безопасность батчей в продакшенеsenior
- CI enforcement и RUM: делаем бюджеты рабочимиmiddle
- V8 JIT-пайплайн, HTTP-приоритеты и безопасность bundlesenior
- Цикл performance: дисциплина, а не проектjunior
- Классификация и исправление: сопоставление family bottleneck с методамиmiddle
- Observability-стек и CI gates: ловить регрессии до выпускаmiddle
- От инцидента к enforcement: SLO burn до верифицированного исправления за 35 минутmiddle
- Культура, экономика и масштаб performancesenior
- At-most-once, at-least-once, exactly-once: три контракта доставкиjunior
- Три ножки сбоя — где реально происходят дубликаты и потериmiddle
- Consumer-side dedup: самый дешёвый путь к exactly-once processingmiddle
- Kafka exactly-once semantics: idempotent producer и транзакцииmiddle
- SQS visibility timeout, DLQ и outbox patternmiddle
- Exactly-once в production: impossibility-доказательство, гибридные паттерны и реальные инцидентыsenior
- Что такое OAuth и почему пароли — не ответjunior
- Authorization code flow с PKCEmiddle
- Валидация ID-токена и управление JWKS-кешемmiddle
- Ротация refresh-токенов и scope-based least privilegemiddle
- Sender-constrained токены: DPoP и mTLSsenior
- OAuth в production: audience атаки, observability и реальные провалыsenior