Знакомство с Brew

Постер кофейни Brew

Первый день в Brew. Офис разработки — второй этаж над самой первой кофейней сети: внизу шумит кофемолка, наверху — дюжина столов, и запах эспрессо здесь никого не удивляет.

Дмитрий
Ноутбук выдали, пропуск сработал — для десяти утра уже неплохо. Я Дмитрий, тимлид бэкенда. Экскурсия короткая: вся наша система — вот.
Одна база?
Дмитрий
Одна база, девять таблиц: заказы, позиции, меню, клиенты, склад. Люди почему-то ждут, что за сетью кофеен стоит что-то грандиозное.
Ботыр
И outbox. Про outbox он всегда забывает, а это лучшая наша таблица — поймёшь ближе к зиме. Я Ботыр, второй бэкендер, теперь нас трое. Совет на первую неделю: не верь здесь слову «просто». «Просто добавь колонку», «просто поправь цену» — у нас так касса вставала.
Дмитрий
Это было один раз.
Ботыр
Это было два раза.

Из-за перегородки, не поворачиваясь от двух мониторов, подаёт голос мужчина с литровой кружкой без единой надписи.

Павел
Павел. База — моя. Песочницу подниму к обеду. Прод не проси.
А если очень нужно?
Павел
Особенно если очень нужно.
Дмитрий
С Павлом граница простая: всё, что внутри сервера, — его территория. Всё, что ходит к серверу с запросами, — наша. Где именно проходит эта граница, я буду показывать весь год. Ещё есть Евгений из маркетинга, этажом выше: он появляется со словами «маленькая доработка», и это ещё ни разу не было правдой.

Дмитрий разворачивает ноутбук. На экране — таблица заказов, первая строка подсвечена.

Дмитрий
Смотри. Заказ номер один: Алиса Иванова, пятнадцатое января, девять ноль-ноль. Капучино и колд брю, статус paid. Первый заказ в истории Brew — Эмиль, наш основатель, до сих пор хранит бумажный чек в рамке. На этой строке я покажу тебе примерно всё: типы, джойны, транзакции, индексы.
Целый курс — на одном заказе?
Дмитрий
На одном заказе, одной базе и одном годе. Запомни два вопроса, с которых здесь начинается любой разбор: «покажи запрос» и «что база ответила». К концу года эти девять таблиц поедут потоком в соседнюю систему — и открывать этот поток будешь ты. А начнём с вопроса попроще: что вообще происходит на том конце сокета, когда приложение говорит с Postgres.

Вопрос про сокет — тема следующего юнита. Этот — не про SQL. Это карта: что такое Brew, что мы построим за курс, чем всё закончится и какие темы пройдём по дороге. Дальше каждый юнит будет открываться конкретной болью бизнеса и закрывать её одним приёмом Postgres — но сперва стоит увидеть маршрут целиком, чтобы не чувствовать, что тебя бросили в код с первой строки.

Что такое Brew

Brew начинался как одна кофейня, а теперь становится сетью: московская точка (BREW-CENTRAL) работает, вторая — питерская (BREW-NORTH) — уже заведена в базе и ждёт открытия, склад под неё заполняется. Плюс общее меню, общая база клиентов и собственное приложение — касса, сайт, промо-рассылки, отчёты. Всё это ходит запросами в ту самую «одну базу» с экскурсии. Дальше Brew будет расти — и это не просто фон. Чем больше Brew, тем дороже ошибка: то, что на одной кассе было незаметно, на сети оборачивается потерянными заказами и кассой, которая встала в обед. Поэтому боль в курсе нарастает от модуля к модулю вместе с самим Brew.

Данные, с которыми ты будешь жить весь курс, — те самые, из экскурсии Дмитрия: меню напитков (drinks), клиенты (customers), заказы (orders) и их позиции (order_items), кофейни (shops), остатки по точкам (inventory), блог (articles) и outbox — та самая таблица Ботыра, через которую события заказа уходят во внешний мир. Один и тот же набор таблиц проходит через весь курс: от первого подключения до финального капстона.

Якорь, который Дмитрий подсветил на экране, — заказ #1 Алисы Ивановой — выбран не для красоты. К этой строке курс будет возвращаться снова и снова: на ней мы посмотрим, как работают JOIN'ы, что происходит при конкурентном обновлении, как читать план запроса. Когда дальше встретишь слово «заказ» и оно покажется слишком общим — вспоминай заказ #1.

Что построим и чем закончим

Маршрут курса — это и маршрут твоего роста в Brew. В начале ты новичок, который склеивает SQL строками (и открывает дыру для инъекции) и роняет пул соединений под первой же нагрузкой. В конце ты инженер, который защищает инварианты прямо в схеме, читает EXPLAIN вместо догадок и пишет ретрай-петлю на сериализационный конфликт.

Финал курса — конкретный, и Дмитрий его уже пообещал. Ты сам откроешь PUBLICATION и передашь поток изменений Brew в соседний курс kafka-cookbook: Postgres отдаёт эстафету, Kafka принимает. Две кофейные истории — один мир, одна модель данных. Поэтому базовые таблицы Brew в этом курсе байт-в-байт совпадают со схемой соседнего: переименуешь колонку — и эстафета сломается. Об этом будет отдельный разговор в капстоне, а пока просто запомни, что у курса есть выход наружу, а не только внутренняя кухня.

Карта курса

Одиннадцать модулей, выстроенных по нарастанию сложности. Грубо — снизу вверх:

  • Подключение и ориентация (этот модуль) — клиент и сервер, песочница, psql, подключение из Go, конвейер sqlc, жизнь соединения и пул.
  • Типы данных — какой тип выбрать и почему: numeric против float для денег, timestamptz для времени, uuid и uuidv7, enum/массивы и введение в jsonb.
  • Схема и ограничения — ключи, NOT NULL, внешние ключи, UNIQUE/CHECK, генерируемые столбцы и мышление миграций: какой ALTER мгновенный, а какой кладёт кассу.
  • CRUD-беглость — INSERT … RETURNING, keyset-пагинация, безопасные UPDATE/DELETE, upsert через ON CONFLICT и трезвая семантика NULL.
  • Запросы по таблицам — JOIN'ы, агрегация, DISTINCT ON, подзапросы и CTE: здесь данные превращаются в ответы на вопросы бизнеса.
  • Транзакции, MVCC и конкурентность — ACID, ментальная модель MVCC, блокировки строк, уровни изоляции, ретрай на 40001, дедлоки.
  • Индексы и EXPLAIN — производительность через чтение планов: B-tree и порядок столбцов, когда индекс не помогает, GIN, CREATE INDEX CONCURRENTLY.
  • JSONB, массивы и поиск — гибкие данные и поиск внутри БД: containment, SQL/JSON path, полнотекстовый поиск и нечёткий через pg_trgm.
  • Аналитика, оконные функции и LATERAL — running total, ранжирование, top-N на группу, рекурсивные CTE, LATERAL как убийца N+1.
  • Запись, события и серверная логика — MERGE/COPY, очередь задач на SKIP LOCKED, transactional outbox, LISTEN/NOTIFY, триггеры.
  • Use cases — сквозные капстоны с integration-тестами, которые связывают весь курс в работающие приложения, и тот самый CDC-шов наружу.

Чего в курсе нет: репликации, бэкапов, тюнинга сервера, мониторинга парка, ролей и расширений. Это работа того, кто держит базу, а не того, кто пишет к ней запросы. Где урок упрощает то, что в проде делают иначе, он честно говорит об этом в секции «Заборчик».

Как устроен курс

Весь курс работает против одной песочницы — контейнер Postgres 18 плюс Adminer, поднимается одной командой из корня репозитория. База одна, brew, на весь курс: каждый юнит докатывает свою схему поверх общей через make db-reset. Отдельный контейнер на юнит не нужен.

Каждый юнит оставляет наблюдаемый след: изменённые строки, план, который можно прочитать, публикацию, которую можно стримить. SQL мы пишем руками — он не растворяется в построителе запросов; для большинства юнитов работает конвейер «query.sql руками → sqlc generate → типизированный pgx-код», и это позвоночник курса (подробно разберём его через несколько юнитов, когда дойдём до типизированных запросов). А когда уроку нужна интерактивность, системные колонки или EXPLAIN, мы откладываем sqlc и пишем psql-скрипты или сырой pgx — выбираем возможность, а не инструмент.

Важная деталь, которая будет всплывать постоянно: вывод демо в README — настоящий и детерминированный. Никаких now(), uuid-значений или случайных чисел в stdout. Если число напечатано, оно одинаково на любой машине и при любом числе прогонов — поэтому ему можно верить и сверять байт-в-байт. Это не педантизм ради педантизма: воспроизводимость — то, что отличает «у меня на машине работает» от «работает у всех».

Как устроен каждый юнит

Юниты собраны по одному шаблону, и узнать его стоит сейчас — дальше он повторяется шесть десятков раз. Урок открывается болью бизнеса Brew (пропал напиток из меню, отчёт не сошёлся на копейки, касса встала под нагрузкой), потом концепт собирается в прозе по стадиям — почему, прежде чем как. Затем идёт раздел ## Запуск с настоящим выводом демо, который мы тут же читаем обратно по фактам. А в самом конце каждого юнита — две секции, которые стоит объяснить отдельно, потому что они повторяются везде.

«Заборчик» — это граница упрощений. Любой учебный пример что-то срезает ради ясности: одна база на весь курс, sslmode=disable, пароль brew/brew прямо в строке подключения. «Заборчик» — место, где урок честно показывает, где именно он упростил и как то же самое делают в проде (часто — словами «а твой DBA сделал бы так»). Название буквальное: за этим забором заканчивается учебная песочница и начинается продакшен. Читай его, чтобы не унести упрощение из урока в боевой код — это та разница, на которой потом горят на ревью.

«Что забрать с собой» — три-четыре буллета с выжимкой урока: то, что должно остаться в голове, когда детали забудутся через месяц. Если просматриваешь юнит по диагонали или возвращаешься к нему как к шпаргалке — читай хотя бы это.

Что показывает наш код

Первый контакт. Подключаемся к песочнице, спрашиваем у сервера его версию (убедиться, что на том конце именно Postgres 18) и делаем перепись мира Brew — проверяем слова Дмитрия: сколько строк лежит в каждой из 9 таблиц канона после seed. Никакого SQL-героизма, просто «оглянись по сторонам, прежде чем браться за дело».

query.sql — два запроса, написанных руками:

sql
-- name: ServerVersion :one
SELECT version();
 
-- name: BrewWorld :many
-- Перепись мира Brew: сколько строк в каждой таблице канона.
SELECT entity, n FROM (
    SELECT 1 AS ord, 'customers'::text     AS entity, count(*) AS n FROM customers
    UNION ALL SELECT 2, 'drinks',               count(*) FROM drinks
    UNION ALL SELECT 3, 'articles',             count(*) FROM articles
    UNION ALL SELECT 4, 'orders',               count(*) FROM orders
    UNION ALL SELECT 5, 'outbox',               count(*) FROM outbox
    UNION ALL SELECT 6, 'processed_outbox_ids', count(*) FROM processed_outbox_ids
    UNION ALL SELECT 7, 'shops',                count(*) FROM shops
    UNION ALL SELECT 8, 'order_items',          count(*) FROM order_items
    UNION ALL SELECT 9, 'inventory',            count(*) FROM inventory
) w
ORDER BY ord;

main.go остаётся тонким — подключиться, выполнить два типизированных запроса, напечатать результат:

go
pool, err := pg.NewPool(ctx)            // пул соединений к песочнице
queries := db.New(pool)                 // типизированная обёртка из sqlc
version, err := queries.ServerVersion(ctx)
world, err := queries.BrewWorld(ctx)    // перепись: таблица → строк

Запуск

Подними песочницу (из корня репозитория) и накати канон Brew:

sh
docker compose up -d
make lecture L=00-getting-connected/00-01-meet-brew T=db-reset

Запусти демо:

sh
make lecture L=00-getting-connected/00-01-meet-brew

(T=run — значение по умолчанию, поэтому без T=... сразу запускается демо.)

Вывод:

plaintext
Сервер: PostgreSQL 18.4 on aarch64-unknown-linux-musl, compiled by gcc (Alpine 15.2.0) 15.2.0, 64-bit
 
Мир Brew — 9 таблиц канона. Что лежит в них после seed:
 
ТАБЛИЦА               СТРОК
customers             3
drinks                5
articles              2
orders                3
outbox                0
processed_outbox_ids  0
shops                 2
order_items           4
inventory             5
 
Итого 24 строки — на этих данных поедет весь курс.

Читаем вывод обратно. Во-первых, версия начинается с PostgreSQL 18 — на том конце сокета именно та версия, на которую рассчитан курс (хвост строки про архитектуру и gcc у тебя может отличаться, это зависит от машины, на которой собран образ postgres:18-alpine). Во-вторых, в каноне 9 таблиц, и часть из них пока пустые: outbox и processed_outbox_ids ждут модуля про события — их мы наполним сами. В-третьих, заказов ровно три, и среди них тот самый заказ #1 Алисы; пять напитков в меню и две кофейни — действующая московская и питерская, которая ждёт открытия, — это и есть мир, в котором мы проживём следующие десять модулей.

Заборчик

  • Курс — для тех, кто пишет приложение, а не администрирует сервер. Репликация, бэкапы, тюнинг, max_connections, мониторинг — зона DBA, и почти каждый юнит будет явно отмечать эту границу. Ты на том конце сокета клиент, а не оператор сервера.
  • Песочница — это docker compose на твоём ноуте. sslmode=disable, пароль brew/brew в открытой строке подключения, одна общая база — всё это годится только локально. В проде соединение шифруют, пароль приходит из секрет-менеджера, а окружения разведены по разным базам.
  • Одна база brew на весь курс — учебное упрощение. На реальном проекте make db-reset (внутри — TRUNCATE) по живым данным не запускают никогда.

Что забрать с собой

  • Brew — сеть кофеен; ты её бэкенд-разработчик. Весь курс — это рост от новичка, который роняет пул, до инженера, который открывает PUBLICATION и отдаёт поток в kafka-cookbook.
  • Данные одни на весь курс: меню, клиенты, заказы, остатки, outbox. Осязаемый якорь — заказ #1 Алисы, к нему будем возвращаться.
  • Работаем против одной песочницы; вывод демо детерминированный и сверяется байт-в-байт, поэтому числам можно верить.

Дальше — юнит 00-02 «Клиент, сервер и песочница»: первый технический шаг. Что вообще находится на том конце сокета, чем сервер отличается от клиента — и как поднять локальную копию, которую не страшно сломать.

·Модуль 01

Этот урок ещё впереди

Курс изучается по порядку — чтобы открыть этот шаг, сначала завершите предыдущие. Так контекст накапливается без пропусков.

/ вы пытались открыть
Подключение и ориентация / Знакомство с Brew