Читай реальную схему, конфиг и код, затем прими решение: анти-паттерн блоба-в-БД, запрос LIKE против инвертированного индекса, best-effort двойная запись и путь загрузки через presigned-ссылку. Выбери изменение, под которым подпишется senior.
SDSenior◷ 14 min
Уровень
ОсновыJuniorMiddleSenior
Баги хранения живут в схеме и коде, не в прозе: тип колонки, запрос с ведущим wildcard, две записи без общей транзакции, поток байтов через не тот уровень. Читай каждый сниппет, рассуждай о том, где данные и гарантии реально находятся, и выбери починку, под которой подпишется senior-инженер.
Потренируй цикл, что ты гоняешь на дизайн- или схема-ревью: найди данные и гарантию в коде, затем выбери изменение, которое свойства хранилища реально поддерживают.
Сниппет 1 — схема
CREATE TABLE attachments ( id uuid PRIMARY KEY, owner_id uuid NOT NULL, filename text NOT NULL, content bytea NOT NULL, -- сырые байты файла, до 25 МБ created_at timestamptz NOT NULL DEFAULT now());
Викторина
Completed
Эта таблица хранит файлы по 25 МБ в колонке `content` BYTEA. Какова senior-поправка?
Heads-up Эта единственная выгода затмевается раздутыми бэкапами, отравленным буфер-кешем, раздутым WAL и голоданием пула, когда строки по 25 МБ churn-ятся. Перенеси байты в объектное хранилище и держи колонку-ключ.
Heads-up Сжатие чуть уменьшает байты, но оставляет каждую структурную проблему (бэкапы, кеш, WAL, пул) нетронутой. Починка — убрать байты из БД, а не сжимать их.
Heads-up Ты не индексируешь блобы по 25 МБ, и индекс не адресовал бы затраты на бэкапы/кеш/WAL. Поправка — метаданные в БД, байты в объектном хранилище.
Сниппет 2 — поисковый запрос
-- поиск товаров, 3 000 000 строк, B-tree индекс на titleSELECT * FROM productsWHERE title LIKE '%' || :term || '%'ORDER BY created_at DESC;
Викторина
Completed
Этот запрос отваливается по таймауту под нагрузкой на 3 млн строк несмотря на B-tree индекс на `title`. Почему и какова починка?
Heads-up Узкое место — полный скан LIKE, а не сортировка. Даже отсортировав, ты уже прочитал каждую строку. Инвертированный индекс убирает скан; индекс на created_at сам по себе нет.
Heads-up Никакой объём памяти не делает B-tree пригодным для LIKE с ведущим wildcard — нет префикса для seek. Структурная починка — инвертированный индекс, отображающий термины в документы.
Heads-up Сужение колонок урезает передачу, но запрос всё ещё сканирует 3 млн строк ради подстрочного матча. Починка — сам паттерн доступа: инвертированный индекс, а не LIKE.
Сниппет 3 — путь записи
async function publishPost(post) { await db.insert("posts", post); // источник истины await search.index("posts", post); // сделать ищущимся return post; // нет общей транзакции}
Викторина
Completed
Какой латентный баг тут и какова надёжная починка?
Heads-up Порядок не помогает: если процесс падает после db.insert, но до search.index, или вызов индекса бросает, пост есть, но не ищется, без записи о потере. Нет транзакции через две системы.
Heads-up Транзакция БД не может охватить базу и поисковый движок — это отдельные системы без общего коммита. Нужна строка outbox, коммитящаяся атомарно с постом, или CDC с WAL.
Heads-up Это и есть best-effort анти-паттерн — он делает тихое расхождение хуже, пряча провалы. Выведи индекс из журнала коммитов, так что коммит данных гарантирует индексацию.
Сниппет 4 — endpoint загрузки
// обработчик загрузки на stateless-уровне приложенияapp.post("/upload", async (req, res) => { const buf = await readEntireBody(req); // буферизует весь файл в памяти приложения await objectStore.put(key(req), buf); // приложение проксирует каждый байт в S3 res.json({ ok: true });});
Викторина
Completed
Загрузки крупных файлов OOM-ят серверы приложения и насыщают их egress. Какова починка, что держит уровень приложения stateless и без пропускной способности?
Heads-up Стриминг избегает OOM, но приложение всё ещё проксирует каждый байт, насыщая egress и сжигая соединение на загрузку. Presigned-ссылки убирают приложение с пути байтов целиком.
Heads-up Масштабирование прокси лишь умножает счёт за пропускную способность и давление на пул; приложение всё ещё на пути байтов. Починка — убрать его с пути presigned-ссылкой.
Heads-up Это возвращает каждый провал блоба-в-БД (бэкапы, буфер-кеш, WAL, пул). Починка — загрузка прямо в объектное хранилище через presigned-ссылку, с метаданными в БД.
Вспомните перед уходом
01
Два хранилища должны остаться в синхроне после записи. Почему await обоих вызовов по порядку не делает их согласованными и что делает?
02
Почему загрузка через presigned-ссылку — починка для уровня приложения, что OOM-ит и насыщает egress на крупных загрузках?
Итог
Каждое решение хранения в этом разделе читаемо прямо со схемы и кода. Колонка BYTEA/BLOB под крупные файлы — анти-паттерн блоба-в-БД — убери её, храни байты в объектном хранилище под ключом, держи только маленькие метаданные. LIKE с ведущим wildcard не может использовать B-tree и полностью сканирует таблицу; починка — инвертированный индекс (поисковый движок или Postgres tsvector/GIN). Две записи в две системы без общей транзакции тихо расходятся при любом падении или провале — выведи второе хранилище из коммита БД через транзакционный outbox или CDC, никогда best-effort. А обработчик загрузки, что буферизует или проксирует байты, OOM-ит и насыщает уровень приложения — убери приложение с пути байтов presigned-ссылкой, чтобы клиент грузил прямо в хранилище. Senior-привычка — найти, где байты и гарантии реально живут, и выбрать изменение, что уважает свойства хранилища, а не заклеивает цену.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.