# ITMO DB lesson 3

Занятие 3. NoSQL: документоориентированные и key-value СУБД — MongoDB и Redis

38 slides, by Olga1507Rub. https://easeweb.host/movie/itmo-db-lesson-3/1/

## Slide 1. NoSQL: документы и ключ-значен
(id 164, https://easeweb.host/movie/itmo-db-lesson-3/1/)
- Университет ИТМО · Школа разработки видеоигр · 11.03.04 ТРКИ
- NoSQL: документы
  и ключ-значение
- Занятие 3. MongoDB и Redis: проектирование схем документов и структур ключей
- Блок 1. Хранилища данных · Санеев Святослав Олегович · 2026

## Slide 2. План занятия
(id 165, https://easeweb.host/movie/itmo-db-lesson-3/2/)
- ЗАНЯТИЕ 3
- План занятия
- 1
- MongoDB
- Документ и $jsonSchema, embedding и referencing, owned / снимок / текущая копия, массивы, транзакции, применение в играх ·
  3–12
- 2
- Redis
- Типы и семейства ключей, TTL и инвалидация, лидерборд, память и вытеснение ·
  13–17
- 3
- Сравнение с реляционной схемой
- Недоступность против потери, что остаётся в PostgreSQL · слайды 18–19; итоги и вопросы ·
  20–21.
- Приложение
- [button "▶ П1–П8: команды к слайдам"] -> slide 22 "П1 Validator и BSON-типы чисел"
- [button "▶ П9–П17: практические паттерны"] -> slide 30 "П9. Индексы MongoDB: правило ESR"

## Slide 3. Глоссарий: что как называется
(id 166, https://easeweb.host/movie/itmo-db-lesson-3/3/)
- 1 · MONGODB · ГЛОССАРИЙ
- Глоссарий: что как называется
- В теории
- В MONGODB
- ПРИМЕР
- База данных
- база данных
- game_db
- BSON (Binary JSON)
- Двоичный формат хранения документов
- Отношение
- коллекция (Collection)
- item_configs
- Кортеж
- документ(Document)
- один предмет
- Атрибут
- поле(Field)
- base_price
- Первичный ключ
- _id
- "sword_dawn"
- Схема таблицы
- $jsonSchema (если задали)
- required, bsonType, enum
- Соединение отношений
- $lookup
- агрегация для левого внешнего соединения
- Выборка / Проекция
- $match / $project
- Фильтрация строк и выбор нужных полей
- Индекс
- Индекс (B-Tree / WiredTiger)
- Документ — граница совместных данных и атомарной записи.

## Slide 4. Три предмета с разным набором
(id 167, https://easeweb.host/movie/itmo-db-lesson-3/4/)
- 1 · MONGODB · ЗАЧЕМ ЭТО НУЖНО
- Три предмета с разным набором характеристик
- В реляционной таблице
- В коллекции документов
- items
  id name type damage crit heal block
  1 Клинок зари weapon 45 0.15 NULL NULL
  2 Малое лечение potion NULL NULL 120 NULL
  3 Щит зари shield NULL NULL NULL 30

```sql
{ _id: 1, type: "weapon", damage: 45, crit_chance: 0.15, name: "Клинок зари" }
{ _id: 2, type: "potion", heal_amount: 120, name: "Малое лечение"}
{ _id: 3, type: "shield", block: 30, name: "Щит зари"}
```
- Правило «у зелья есть heal, нет damage»
  пишется CHECK. Подтипы с PK = FK тоже законны.
- Вариант 3: EAV
- Вариант 4: jsonb
- Вопрос к слайду
- Таблица (id, атрибут, значение).
  выборки по атрибутам усложняются.
- Колонка jsonb в PostgreSQL: CHECK, GIN и индексы по выражению.
- Если вариант 4 работает, зачем отдельная база?

## Slide 5. «Схемы нет» означает «схема в
(id 168, https://easeweb.host/movie/itmo-db-lesson-3/5/)
- 1 · MONGODB · СХЕМА
- [button "run"] -> slide 22 "П1 Validator и BSON-типы чисел"
- Без схемы ошибки не будет
- Если схему записать в базу

```json
db.item_configs.insertOne({
  nmae: "Топор",      // опечатка
  type: "waepon",    // опечатка
  base_price: "сто"  // строка вместо числа
})
```

```json
validator: { $jsonSchema: {
  required: ["name","type","base_price"],
  properties: {
    type: { enum: ["weapon","potion","shield"] },
    base_price: { bsonType: "int", minimum: 0 }
}}}
// 100 → int: принят;  Double(100) → код 121
```
- validationLevel
- validationAction
- createIndex unique
- strict: проверять все вставки и обновления.
  moderate: пропускать updates уже невалидных.
  off: Не проверяет ничего
- error — отклонять (код 121).
  warn — пропускать, писать в лог сервера.
- _id уникален по умолчанию. Уникальность name это явный индекс { name: 1 }, unique; дубль даст код 11000.
- Три разных механизма. Внешних ключей нет ни в одном из них.
- [button "▶ П10: миграция"] -> slide 31 "П10. Версия схемы и ленивая миграция"

## Slide 6. Embedding: вложить внутрь
(id 169, https://easeweb.host/movie/itmo-db-lesson-3/6/)
- 1 · MONGODB · ДОКУМЕНТ · 1 ИЗ 2
- Embedding: вложить внутрь

```json
{ _id: "sword_dawn", name: "Клинок зари",
  base_price: 100,
  stats: { damage: 45, crit_chance: 0.15 },
  effects: [
    { code: "burn", damage_per_sec: 5 },
    { code: "vampire", percent: 10 }
  ] }
```
- Одно чтение может вернуть весь предмет.
  Для вложенного $lookup не нужен; projection берёт только нужные поля.
  Одна запись атомарна.
  Несколько вложенных полей одного документа меняются целиком или никак — без транзакции.
- ВЛОЖИТЬ, ЕСЛИ
- ВЛОЖИТЬ, ЕСЛИ
- ВЛОЖИТЬ, ЕСЛИ
- Читаются вместе
- Это отношение «состоит из»
- Количество ограничено
- Если вы ни разу не запрашиваете вложенное отдельно от родителя — ему там и место.
- Собственные stats принадлежат предмету. Общий burn в effects может быть текущей копией.
- Названа численная граница (maxItems: 5) и механизм её соблюдения на всех путях записи.
- Вложение не обязательно копия. Здесь stats owned, общего оригинала нет. В П2 это уже другой смысл: effects.burn повторяет общий факт и требует синхронизации.

## Slide 7. Referencing: вынести наружу
(id 170, https://easeweb.host/movie/itmo-db-lesson-3/7/)
- 1 · MONGODB · ДОКУМЕНТ · 2 ИЗ 2
- [button "← зан. 2: 3НФ"] -> presentation "ITMO DB lesson 2" (lesson-2)
- [button "run"] -> slide 23 "П2 Ссылка, текущая копия и снимок"
- Referencing: вынести наружу

```sql
// item_configs
{ _id: "axe_dusk", effect_ids: ["burn", "freeze"] }
// item_effects
{ _id: "burn", title: "Горение", damage_per_sec: 6 }

{ $lookup: { from: "item_effects", localField: "effect_ids",
            foreignField: "_id", as: "effects" } }
```
- Правим общий факт в одном месте.
  Новые чтения ссылки видят 6. Уже загруженный кэш сам не обновится.
  Платим сборкой данных.
  $lookup соединяет на сервере в одном запросе
- ВЫНЕСТИ, ЕСЛИ
- ВЫНЕСТИ, ЕСЛИ
- ВЫНЕСТИ, ЕСЛИ
- Растёт без предела
- Живёт своей жизнью
- Копией трудно управлять
- Чайлдов может быть неограниченно много: parent_id у ребёнка и индекс по нему, а не бесконечный массив ID.
- Дочерняя сущность существует без родителя, ссылается на неё не он один, архивируется отдельно.
- Значение меняется, расхождение заметит пользователь. Нужны источник истины, допустимый lag и сверка.
- Внешних ключей нет. effect_ids: ["no_such_effect"] $lookup вернёт []. Если урон зависит только от эффекта, копия в каждом предмете даёт аномалии обновления это как аналогия с 3НФ.
- [button "▶ П9: индексы"] -> slide 30 "П9. Индексы MongoDB: правило ESR"

## Slide 8. Embedding или referencing: таб
(id 171, https://easeweb.host/movie/itmo-db-lesson-3/8/)
- 1 · MONGODB · РЕШЕНИЕ
- Embedding или referencing: таблица решений
- ВОПРОС О ДАННЫХ
- ОТВЕТ
- РЕШЕНИЕ
- Читаются вместе?
- да
- вложить
- Меняются вместе, одной операцией?
- да
- вложить
- Отношение «состоит из», «принадлежит»?
- да
- вложить
- Удаляются вместе с родителем?
- да
- вложить
- Кардинальность на дочерней стороне высокая?
- да
- вынести
- Массив растёт без верхней границы?
- да
- вынести
- Дочернее живёт без родителя?
- да
- вынести
- Пишутся в разное время, запись частая?
- да
- вынести
- Расхождением копий трудно управлять?
- да
- вынести
- Это эвристика.
  Сначала инварианты и пределы роста, затем чтение и запись, индексы, конкуренция, допустимый лаг.

## Slide 9. Три вида дублируемых данных
(id 172, https://easeweb.host/movie/itmo-db-lesson-3/9/)
- 1 · MONGODB · КОГДА КОПИЯ — НЕ ДУБЛЬ
- [button "← зан. 2: шесть вопросов"] -> presentation "ITMO DB lesson 2" (lesson-2)
- [button "run"] -> slide 23 "П2 Ссылка, текущая копия и снимок"
- Три вида дублируемых данных
- НЕИЗМЕНЯЕМЫЕ
- ИСТОРИЧЕСКИЕ
- ТЕКУЩИЕ КОПИИ
- Не меняются по правилам
- Меняются, но копия должна остаться прежней
- Копия должна догонять источник
- ID с запретом изменения.
  Синхронизация не нужна, но гарантию и исправление ошибочного ввода описывают.
- price_paid, level_at_purchase, ник на момент матча.
  Строго говоря, это не копия, а отдельный факт: как было тогда.
- Текущая цена, текущий ник, общий эффект burn.
- Меч: 5 или 6 урона? Собственный stats.damage не синхронизируем. В другой модели effects.burn — копия: 5 при источнике 6 требует обновления; снимок покупки 5 остаётся верным.

## Slide 10. Массив, который растёт без пре
(id 173, https://easeweb.host/movie/itmo-db-lesson-3/10/)
- 1 · MONGODB · АНТИПАТТЕРН
- [button "run"] -> slide 24 "П3 Массив с численной границе"
- Массив, который растёт без предела
- Так делать нельзя
- Ограничить массив на месте

```mongodb
{ _id: 1042, nickname: "Klinok",
  feed: [
    { at: ..., text: "матч завершён" },
    { at: ..., text: "матч завершён" },
    ... ещё 300 тысяч элементов
  ] }
```

```mongodb
$push: { recent_feed: {
  $each:  [ новое_событие ],
  $sort:  { at: -1 },
  $slice: 5
}}
// после операции — не больше пяти
// validator maxItems: 5 отклонит 6-й (121)
```
- ПРОБЛЕМА 1
- ПРОБЛЕМА 2
- ПРОВЕРКА
- Растёт цена обработки
- Лимит 16 mb
- Проверка в одну фразу
- Больше данных, индексации и конкуренции за документ. Projection сокращает ответ, но не обещает дешёвый I/O.
- Жёсткий предел; вложенность в 100 уровней.
- Назовите число N и что обеспечивает его на всех путях записи. «Удаляем в конце сезона» — не ответ.
- История — отдельно. Полная лента — коллекция с player_id и индексом { player_id: 1, at: -1 }. $slice отбрасывает элементы, но не архивирует их. Измеряйте $bsonSize и explain.

## Slide 11. Атомарность и транзакции
(id 174, https://easeweb.host/movie/itmo-db-lesson-3/11/)
- 1 · MONGODB · ТРАНЗАКЦИИ
- [button "← зан. 1: BEGIN … COMMIT"] -> presentation "ITMO DB Lesson 1" (lesson-1)
- Атомарность и транзакции
- Одна операция над документом — атомарна
- Несколько документов, всё или ничего

```mongodb
updateOne({ _id: "sword_dawn" }, {
  $set: { "effects.0.damage_per_sec": 7,
          base_price: 110 } })
// два поля, одна атомарная операция
// find → вычислить → update НЕ атомарно:

```

```mongodb
s = db.getMongo().startSession()
tx = s.getDatabase("game_db")
s.startTransaction()
  tx.item_effects.updateOne(… burn …)
  tx.item_configs.updateMany(… копии …)
s.commitTransaction()  // или abort
```
- ОГРАНИЧЕНИЕ
- ЦЕНА
- ОДНА КОЛЛЕКЦИЯ
- Нужен replica set
- Дороже одной записи
- Тоже нужна транзакция
- Standalone не поддерживает. В стенде rs0 из одного узла: транзакции есть, отказоустойчивости нет.
- Ресурсы, длительность, конфликты. Embedding иногда упрощает атомарность, если смысл и рост позволяют.
- Два документа одной коллекции не меняются как одно целое. updateMany атомарен для каждого документа, не для набора.
- Частые транзакции - скорее ошибка модели. Повод проверить требования, границы документов и нагрузку.

## Slide 12. Применение в игростроении
(id 229, https://easeweb.host/movie/itmo-db-lesson-3/12/)
- 1 · MONGODB · КЕЙСЫ
- Применение в игростроении
- [card] EPIC GAMES · 2018 · Fortnite · Профили, статистика, предметы, матчмейкинг. · fortnite.com · postmortem ↗ -> https://www.fortnite.com/news/postmortem-of-service-outage-at-3-4m-ccu
- [card] SEGA HARDLIGHT · Sonic Dash, Crazy Taxi: City Rush · Аккаунты игроков и онлайн-функции. · casestudies.com · MongoDB ↗ -> https://www.casestudies.com/company/mongodb/case-study/sega-hardlight-scales-to-millions-of-mobile-gamers-with-mongodb-atlas
- [card] FACEIT · Киберспорт-платформа · Профили, команды, турниры, статистика · medium.com ↗ -> https://tirth1272.medium.com/how-gaming-is-reinvented-using-mongodb-91d33e2cffae
- [card] SQUARE ENIX · 2013 · Аналитика игр · Телеметрия: выстрелы, время на уровнях. · itpro.com ↗ -> https://itpro.com/strategy/19898/square-enix-selects-mongodb-solve-big-data-conundrum
- [card] FUTUREPLAY · Merge Gardens · Live ops, события и конфигурации · mongodb.com · MongoDB ↗ -> https://www.mongodb.com/solutions/customer-case-studies/futureplay-games
- [card] BEAMABLE · Бэкенд · Учётные записи игроков и лидерборды как сервис для игр. · mongodb.com · MongoDB ↗ -> https://www.mongodb.com/solutions/customer-case-studies/beamable#results
- Общее: документ на игрока, событие или конфигурацию; чтение по ключу, шардирование по ID игрока.

## Slide 13. Типы значений и пути доступа
(id 175, https://easeweb.host/movie/itmo-db-lesson-3/13/)
- 2 · REDIS · ТИПЫ ДАННЫХ
- [button "← зан. 1: Redis"] -> presentation "ITMO DB Lesson 1" (lesson-1)
- [button "▶ Запустить: П4"] -> slide 25 "П4 Типы, secondary index и SC"
- Типы значений и пути доступа
- СТРОКА
- ХЕШ
- МНОЖЕСТВО СО СЧЁТОМ
- СПИСОК
- string
- hash
- sorted set
- list
- SET session:9f3a 1042 EX 1800
  Сессия, флаг, счётчик, сериализованный объект. Умеет INCR — атомарно.
- HSET player:1042:profile level 13
  Кэш профиля: HGET / HSET одного поля без перезаписи всего объекта.
- ZADD leaderboard:season:7 1500 player:1042
  Лидерборд, очередь по времени, отложенные задачи. Порядок держится сам.
- RPUSH matchmaking:queue:ranked 1042
  FIFO, как у матчмейкера на занятии 1. Без ACK: сбой после LPOP теряет задачу.
- У существующего значения один тип. GET по хешу — WRONGTYPE; TYPE покажет тип, но не смысл полей.
  Общего WHERE по полям нет — индекс поддерживают сами. Set region:eu:players хранит ID игроков региона; приложение обновляет его при смене региона и удалении профиля. Для каждого запроса заранее назовите ключ или структуру-индекс и цену её поддержки. Запуск — П4.
- [button "▶ П16: HyperLogLog"] -> slide 37 "П16. HyperLogLog и битовые карты"
- [button "▶ П11: Streams"] -> slide 32 "П11. Надёжная очередь: Redis Streams"

## Slide 14. Семейства ключей — часть схемы
(id 176, https://easeweb.host/movie/itmo-db-lesson-3/14/)
- 2 · REDIS · КЛЮЧИ
- [button "▶ Запустить: П4"] -> slide 25 "П4 Типы, secondary index и SC"
- Семейства ключей — часть схемы
- семейства ключей — сегментов может быть разное число
  session:<token> -> player_id
  player:<id>:profile -> хеш профиля
  player:<id>:inventory -> кэш инвентаря (JSON)
  leaderboard:season:<n> -> sorted set
  matchmaking:queue:<mode> -> list (FIFO)
- Двоеточие ничего не значит.
  Redis его не разбирает и иерархии не строит. Вся польза в том, что по нему удобно фильтровать в SCAN и что человек читает ключ и понимает, что внутри.
- KEYS
- SCAN
- TYPE
- Полный блокирующий обход. Только на крошечном учебном наборе.
- С курсора 0 до возврата 0.
- Текущий тип значения — но не бизнес-смысл полей.
- Схема Redis не только имена. Для каждого семейства: тип и формат, владелец записи, TTL, инвалидация, источник восстановления. Имена следуют записанным шаблонам.
- [button "▶ П14: большие ключи"] -> slide 35 "П14. Один поток команд и большие ключи"

## Slide 15. TTL — не гарантия свежести
(id 177, https://easeweb.host/movie/itmo-db-lesson-3/15/)
- 2 · REDIS · TTL
- [button "▶ Запустить: П5"] -> slide 26 "П5 Цепочка TTL: SET, KEEPTTL,"
- TTL — не гарантия свежести
- Срок жизни
- Живая цепочка: предскажите TTL
- SET session:9f3a 1042 EX 1800
  TTL session:9f3a -> ≈1800
  TTL leaderboard:season:7 -> -1 срока нет
  TTL nosuchkey -> -2 ключа нет
  GET и HGET срок не продлевают
- SET k v1 EX 300 TTL k -> ≈300
  SET k v2 TTL k -> -1
  SET k v3 KEEPTTL TTL k -> -1 (!)
  EXPIRE k 300
  SET k v4 KEEPTTL TTL k -> ≈300
  k = lesson03:ttl:lecture
- SET снимает TTL
- HSET и TTL поля
- Cache-aside
- Обычная перезапись снимает срок. KEEPTTL сохраняет существующий, но не возвращает снятый.
- Срок ключа сохраняется, TTL перезаписанного поля снимается. HEXPIRE — с Redis 7.4.
- Промах → источник → кэш с TTL; после commit — DEL. Гонка: старое чтение запишет кэш после DEL.
- DEL + TTL не исключают устаревшие ответы. Назовите допустимый lag и политику гонок; для строгих требований — версии или чтение из источника. Запуск — П5.
- [button "▶ П13: лавина"] -> slide 34 "П13. Лавина промахов: разброс TTL и замок"

## Slide 16. Лидерборд на sorted set
(id 178, https://easeweb.host/movie/itmo-db-lesson-3/16/)
- 2 · REDIS · ЛИДЕРБОРД
- [button "← зан. 1: лидерборд"] -> presentation "ITMO DB Lesson 1" (lesson-1)
- [button "▶ Запустить: П6"] -> slide 27 "П6 Ничья, nil и повтор ZINCRB"
- [button "▶ SQL-сравнение: П7"] -> slide 28 "П7 Топ-N и ранг на B-tree"
- Лидерборд на sorted set
- ZADD leaderboard:season:7 1500 player:1042
  ZINCRBY leaderboard:season:7 25 player:1042
  атомарно, но повтор — ещё +25
  ZRANGE leaderboard:season:7 0 9 REV WITHSCORES
  топ-10: границы включительны; N=0 — пусто в коде
  ZREVRANK leaderboard:season:7 player:1042
  с нуля; nil — игрока нет
- Сравниваем с индексом, а не с сортировкой.
  B-tree PostgreSQL обновляется инкрементально; топ-10 по индексу эффективен. Точный ранг может стоить дороже — измеряйте (П7).
  Redis: запись и ранг O(log N), диапазон O(log N + M). Цена — память, синхронизация, восстановление.
- ЛОВУШКА
- «РАНЬШЕ — ВЫШЕ»
- ПОМНИТЬ
- Ничья: байты member
- Не дробь в score
- Это производные данные
- Равный score → порядок по байтам member, с REV — обратный. Не по нику и не по времени.
- SQL: rating DESC, achieved_at ASC, player_id ASC. Точности double не хватит.
- Источник истины — матчи в PostgreSQL. Потеря — пересчёт и догон новых матчей.
- Сначала проверьте nil, затем rank + 1. Повтор ZINCRBY не идемпотентен: нужна дедупликация по ID матча или абсолютный рейтинг с версией. Запуск — П6 и П7.
- [button "▶ П12: SET NX"] -> slide 33 "П12. Идемпотентность и блокировка: SET NX"

## Slide 17. Память: лимит и политику выбир
(id 179, https://easeweb.host/movie/itmo-db-lesson-3/17/)
- 2 · REDIS · ПАМЯТЬ
- Память: лимит и политику выбирают явно
- Стенд: опыт только в redis-memory (8mb)
- Пример для чистого кэша
- CONFIG GET maxmemory maxmemory-policy
  -> 8388608, noeviction
  SET lesson03:fill:N <128 KiB> …
  -> OOM command not allowed …
  GET lesson03:fill:0 -> чтение работает
  CONFIG SET maxmemory-policy allkeys-lru
  -> 80 записей, evicted_keys растёт
- maxmemory 2gb
  maxmemory-policy allkeys-lru
  // maxmemory 0 — нет лимита Redis:
  // растёт до лимита контейнера / ОС,
  // возможен OOM kill процесса
- noeviction
- allkeys-lru
- volatile-ttl
- При исчерпанном лимите отклоняет команды роста памяти. GET и DEL работают. Основной redis стенда: 32mb.
- Приближённый LRU по всем ключам, независимо от TTL. Для восстанавливаемого кэша.
- Только ключи с TTL, ближайшие к истечению. Сессии с TTL — тоже кандидаты.
- SELECT не изолирует память и политику. Кэш и важное состояние — разные экземпляры Redis. «Redis сам всё почистит» — неверно: лимит и политику называют явно.
- [button "▶ П17: безопасность"] -> slide 38 "П17. Redis и MongoDB не открывают в интернет"
- [button "▶ П14: большие ключи"] -> slide 35 "П14. Один поток команд и большие ключи"

## Slide 18. Недоступно ≠ потеряно
(id 180, https://easeweb.host/movie/itmo-db-lesson-3/18/)
- 3 · СРАВНЕНИЕ · НЕДОСТУПНОСТЬ
- Недоступно ≠ потеряно
- КЭШ
- Недоступен: PostgreSQL с таймаутом и лимитом нагрузки. Потерян: заполняем заново.
- Деградация
- player:*:profile
- ПРОИЗВОДНОЕ
- Недоступен: сообщаем о задержке. Потерян: пересчёт из матчей, затем догон.
- Задержка
- leaderboard:*
- СОСТОЯНИЕ
- Недоступен: fail closed, 503. Потерян: повторный вход, новая заявка.
- Потеря
- session:*, matchmaking:*
- Сессии: fail closed. Если сессию нельзя проверить, защищённый запрос не пропускают «временно без проверки».
  В стенде Redis без RDB/AOF и теряет данные при перезапуске. AOF everysec может потерять последние записи при аварии и не заменяет backup.
- [button "▶ П15: outbox"] -> slide 36 "П15. Transactional outbox: событие до доставки"

## Slide 19. Что НЕ надо переносить
(id 181, https://easeweb.host/movie/itmo-db-lesson-3/19/)
- 3 · СРАВНЕНИЕ · ГЛАВНЫЙ ВОПРОС ПРАКТИКИ
- [button "← зан. 1: критерии"] -> presentation "ITMO DB Lesson 1" (lesson-1)
- [button "▶ Запустить: П8"] -> slide 29 "П8 Каталог в jsonb: CHECK и G"
- Что НЕ надо переносить
- ДАННЫЕ
- КАНДИДАТ
- ПОЧЕМУ
- players, purchases, инвентарь
- PostgreSQL
- Деньги и учёт: FK, CHECK и транзакции — норма, а не исключение
- matches, match_participants
- PostgreSQL
- Структура стабильна, считается агрегатами и JOIN
- item_configs, events_config
- MongoDB или jsonb
- Сравнить модель, запросы, индексы и цену эксплуатации
- session, profile cache, leaderboard
- Redis или PostgreSQL
- TTL и структуры удобны; проверить нагрузку и потерю
- Довод, который не засчитывается
- «Выбрал MongoDB, потому что гибкая структура».
  У PostgreSQL есть jsonb с CHECK, GIN и индексами по выражению (П8). Засчитываются проверяемые доводы: конкретные операции каталога, нагрузочный опыт на тех же данных и индексах, независимый владелец, план роста.
- «Оставляем в PostgreSQL как jsonb» — полный ответ. Если рядом измеримое условие пересмотра: «p95 каталога выше 50 мс на целевой нагрузке после настройки индексов — сравним на стенде».
- [button "▶ П15: outbox"] -> slide 36 "П15. Transactional outbox: событие до доставки"

## Slide 20. Что забрать с занятия
(id 182, https://easeweb.host/movie/itmo-db-lesson-3/20/)
- ИТОГИ
- Что забрать с занятия
- Занятие 2 не отменяется
- Вложение — не всегда копия
- Зависимости и инварианты проектирует человек и в SQL, и в NoSQL. MongoDB даёт validator и unique, но не FK.
- Owned, исторический снимок и текущая копия — разные смыслы. Для копии нужны источник, обновление и lag.
- Схема есть всегда
- Второе хранилище — это цена
- В MongoDB часть схемы проверяет $jsonSchema. Соглашение о ключах Redis хранится в коде и документации: сам Redis его не проверяет.
- Память, эксплуатация, согласованность, восстановление. «Быстрее» без измерений — не довод.
- Практика 3. Проектирование и наполнение NoSQL-хранилищ
- Обязательно: локальная мини-лабораторная MongoDB + Redis — 1–2 коллекции с validator (± тест), projection и $set, hash профиля, TTL-цепочка, ничья и nil, повтор seed, план восстановления. Проект может остаться на PostgreSQL — с обоснованием и условием пересмотра. 5 критериев × 2 = 10 баллов.

## Slide 21. Контрольные вопросы
(id 183, https://easeweb.host/movie/itmo-db-lesson-3/21/)
- ПРОВЕРКА СЕБЯ
- Контрольные вопросы
- 1
- Что атомарно в одном документе? Чем projection и $set отличаются от read-modify-write?
- 2
- Чем owned-поля отличаются от price_paid и копии общего текущего эффекта?
- 3
- Как гарантировать предел массива и где хранить историю после $slice?
- 4
- Когда транзакция нужна даже в одной коллекции? Почему standalone не подходит, а один узел не даёт HA?
- 5
- Чем maxmemory 0 отличается от лимита с noeviction? Что получает OOM?
- 6
- Почему KEEPTTL не вернёт срок? Чем TTL ключа отличается от TTL поля и от свежести?
- 7
- Как сравнить MongoDB с jsonb, Redis — с SQL-индексом? Что будет при равном score?
- 8
- Что делать при недоступности и при потере кэша, производных данных и состояния?
- Вопрос 8 — начало практики. Опорные ответы — в конспекте, раздел 16. Команды для самостоятельного запуска — приложение П1–П17.

## Slide 22. П1 Validator и BSON-типы чисел
(id 184, https://easeweb.host/movie/itmo-db-lesson-3/22/)
- ПРИЛОЖЕНИЕ · П1 · MONGODB · К СЛАЙДУ 5
- [button "← к слайду 5"] -> slide 5 "«Схемы нет» означает «схема в"
- П1. Validator и BSON-типы чисел

```mongodb
db.item_configs.findOne({ _id: "sword_dawn" },
  { name: 1, base_price: 1 })
db.item_configs.updateOne({ _id: "sword_dawn" },
  { $set: { base_price: 100 } })
db.item_configs.aggregate([{ $match: { _id: "sword_dawn" } },
  { $project: { t: { $type: "$base_price" } } }])
db.item_configs.updateOne({ _id: "sword_dawn" },
  { $set: { base_price: Double(100) } })
```
- Что должно получиться
- { _id: 'sword_dawn', name: 'Клинок зари',
  base_price: 100 }
  matchedCount: 1, modifiedCount: 0
  [ { _id: 'sword_dawn', t: 'int' } ]
  Document failed validation // код 121
  // цена осталась 100 (int)
- Литерал 100
- Double(100)
- Дубль name
- В mongosh целое, помещающееся в Int32, сохраняется как int.
- Целое по смыслу, но BSON double: validator с bsonType int отклоняет запись.
- Валидный документ с новым _id и занятым name даст E11000: unique-индекс создан явно.

## Slide 23. П2 Ссылка, текущая копия и снимок
(id 185, https://easeweb.host/movie/itmo-db-lesson-3/23/)
- ПРИЛОЖЕНИЕ · П2 · MONGODB · К СЛАЙДАМ 7 И 9
- [button "← к слайду 7"] -> slide 7 "Referencing: вынести наружу"
- П2. Ссылка, текущая копия и снимок

```mongodb
db.item_effects.findOne({ _id: "burn" })
db.item_configs.findOne({ _id: "sword_dawn" }, { effects: 1 })
db.purchase_history.findOne({ _id: "purchase-1" })
db.item_configs.aggregate([
  { $match: { effect_ids: { $exists: true } } },
  { $lookup: { from: "item_effects", localField: "effect_ids",
      foreignField: "_id", as: "resolved" } },
  { $project: { effect_ids: 1, "resolved._id": 1,
      "resolved.damage_per_sec": 1 } } ])
```
- Что должно получиться

```mongodb
burn: damage_per_sec 6       // источник
sword_dawn.effects: burn 5  // текущая копия
purchase-1: price_paid 100,
  effect_snapshot 5         // история
axe_dusk:   resolved burn 6, freeze
staff_void: resolved []    // нет FK
```
- Копия отстала
- Снимок верен
- Синхронизация
- Источник 6, копия меча 5: нарушение, если превышен допустимый lag.
- 5 в покупке — факт «как было тогда». Обновлять его вслед за справочником нельзя.
- updateMany с arrayFilters по e.code = burn — для набора копий «всё или ничего» нужна транзакция.
- Здесь effects.burn копия, не owned stats слайда 6. purchase_history в MongoDB лишь иллюстрирует снимок

## Slide 24. П3 Массив с численной границе
(id 186, https://easeweb.host/movie/itmo-db-lesson-3/24/)
- ПРИЛОЖЕНИЕ · П3 · MONGODB · К СЛАЙДУ 10
- [button "← к слайду 10"] -> slide 10 "Массив, который растёт без пре"
- П3. Массив с численной границей

```mongodb
db.player_feeds.updateOne({ _id: 1042 },
  { $set: { recent_feed: [] } })
db.player_feeds.updateOne({ _id: 1042 }, { $push: { recent_feed: {
  $each: [{ seq: 1, detail: "m" }, { seq: 2, detail: "m" },
          { seq: 3, detail: "m" }, { seq: 4, detail: "m" },
          { seq: 5, detail: "m" }, { seq: 6, detail: "m" },
          { seq: 7, detail: "m" }, { seq: 8, detail: "m" }],
  $slice: -5 } } })
db.player_feeds.aggregate([{ $project: {
  seqs: "$recent_feed.seq", bytes: { $bsonSize: "$$ROOT" } } }])
db.player_feeds.updateOne({ _id: 1042 },
  { $push: { recent_feed: { seq: 9, detail: "m" } } })
```
- Что должно получиться

```sql
matchedCount: 1
modifiedCount: 1
[ { _id: 1042, 
  seqs: [ 4, 5, 6, 7, 8 ], 
  bytes: … } ]

Document failed validation   // 6-й элемент
// maxItems: 5 без $slice — отказ
```
- $slice: -5
- maxItems: 5
- $bsonSize
- Восемь событий, осталось пять последних в порядке добавления.
- Validator отклоняет превышение, если путь записи забыл $slice.
- Измеряет BSON документа
- Граница — число и механизм на всех путях записи. Полная история это отдельная коллекция с player_id и индексом { player_id: 1, at: -1 }.

## Slide 25. П4 Типы, secondary index и SC
(id 187, https://easeweb.host/movie/itmo-db-lesson-3/25/)
- ПРИЛОЖЕНИЕ · П4 · REDIS · К СЛАЙДАМ 13–14
- [button "← к слайду 13"] -> slide 13 "Типы значений и пути доступа"
- П4. Типы, secondary index и SCAN

```redis
HGET player:1042:profile level
HINCRBY player:1042:profile level 1
TYPE player:1042:profile
GET player:1042:profile
SMEMBERS region:eu:players
RPUSH matchmaking:queue:ranked 3003
LPOP matchmaking:queue:ranked
SCAN 0 MATCH player:* COUNT 100
```
- Что должно получиться
- "13"
  (integer) 14
  hash
  (error) WRONGTYPE …
  1042 и 2001 (порядок set произволен)
  (integer) 3; затем "1042" (FIFO)
  SCAN: курсор и найденные ключи — без фиксированного порядка
- WRONGTYPE
- Свой индекс
- SCAN до 0
- У значения один тип: хеш читают HGET, а не GET.
- region:eu:players — secondary index, который обновляет приложение.
- Курсор "0" в ответе — конец обхода. Пустая порция концом не является.
- Это данные веб-стенда. При повторном запуске HINCRBY и RPUSH меняют состояние. Для локального стенда — dba/appendix.md; Redis печатает результат каждой команды и после WRONGTYPE.

## Slide 26. П5 Цепочка TTL: SET, KEEPTTL,
(id 188, https://easeweb.host/movie/itmo-db-lesson-3/26/)
- ПРИЛОЖЕНИЕ · П5 · REDIS · К СЛАЙДУ 15
- [button "← к слайду 15"] -> slide 15 "TTL — не гарантия свежести"
- П5. Цепочка TTL: SET, KEEPTTL, EXPIRE

```redis
SET lesson03:ttl:lecture v1 EX 300
TTL lesson03:ttl:lecture
SET lesson03:ttl:lecture v2
TTL lesson03:ttl:lecture
SET lesson03:ttl:lecture v3 KEEPTTL
TTL lesson03:ttl:lecture
EXPIRE lesson03:ttl:lecture 300
SET lesson03:ttl:lecture v4 KEEPTTL
TTL lesson03:ttl:lecture
TTL lesson03:nosuchkey
```
- Что должно получиться
- OK
  (integer) 300 // ≈300
  OK
  (integer) -1 // SET снял срок
  OK
  (integer) -1 // KEEPTTL не вернул
  (integer) 1
  OK
  (integer) 300 // сохранён deadline
  (integer) -2 // ключа нет
- SET без EX
- KEEPTTL
- HSET и TTL
- Снимает TTL: кэш становится «бессмертным».
- Сохраняет существующий срок, но не восстанавливает снятый.
- HSET сохраняет срок ключа, но снимает TTL перезаписанного поля (HEXPIRE — Redis 7.4).
- Предскажите каждый TTL до запуска. Ожидаемо: около 300, −1, −1, около 300. Точные секунды зависят от момента запуска.

## Slide 27. П6 Ничья, nil и повтор ZINCRB
(id 189, https://easeweb.host/movie/itmo-db-lesson-3/27/)
- ПРИЛОЖЕНИЕ · П6 · REDIS · К СЛАЙДУ 16
- [button "← к слайду 16"] -> slide 16 "Лидерборд на sorted set"
- П6. Ничья, nil и повтор ZINCRBY

```redis
DEL lesson03:tie
ZADD lesson03:tie 1720 player:1042 1720 player:2001
ZRANGE lesson03:tie 0 9 REV WITHSCORES
ZREVRANK lesson03:tie player:1042
ZREVRANK lesson03:tie player:missing
ZINCRBY lesson03:tie 25 player:1042
ZINCRBY lesson03:tie 25 player:1042
ZADD lesson03:tie not-a-score player:x
```
- Что должно получиться
- (integer) 0 или 1
  (integer) 2
  1) "player:2001" 2) "1720"
  3) "player:1042" 4) "1720"
  (integer) 1 // место = 1 + 1
  (nil) // проверить до +1
  "1745"
  "1770" // повтор = ещё +25
  (error) … not a valid float
- Ничья
- nil
- Не идемпотентно
- Равный score: порядок по байтам member, с REV — обратный. «2» > «1».
- Отсутствующий игрок: ранг nil. Прибавлять единицу нельзя.
- Повтор доставки результата матча начисляет очки дважды.
- «Раньше достигший — выше» Redis сам не сделает. Храните achieved_at и сортируйте в SQL (П7), а не прячьте время в дробь score.

## Slide 28. П7 Топ-N и ранг на B-tree
(id 190, https://easeweb.host/movie/itmo-db-lesson-3/28/)
- ПРИЛОЖЕНИЕ · П7 · POSTGRESQL · К СЛАЙДУ 16
- [button "← к слайду 16"] -> slide 16 "Лидерборд на sorted set"
- П7. Топ-N и ранг на B-tree

```sql
EXPLAIN (COSTS OFF)
SELECT player_id, rating FROM season_ratings
WHERE season_id = 7
ORDER BY rating DESC, achieved_at ASC, player_id ASC
LIMIT 10;
SELECT 1 + (SELECT count(*) FROM season_ratings r
  WHERE r.season_id = me.season_id AND
    (r.rating > me.rating OR (r.rating = me.rating AND
      (r.achieved_at, r.player_id) <
       (me.achieved_at, me.player_id)))) AS place
FROM season_ratings me
WHERE me.season_id = 7 AND me.player_id = 1042;
```
- Что должно получиться
- План топ-N зависит от данных и статистики;
  при подходящих условиях — проход по индексу.
  place = 11152 (веб-данные)
  Отсутствующий player_id: 0 строк, не место 1.
  Число прочитанных строк смотрите в
  EXPLAIN (ANALYZE, BUFFERS).
- Индекс
- Ничья
- Цена ранга
- (season_id, rating DESC, achieved_at, player_id) — B-tree обновляется инкрементально.
- В веб-данных 900001 и 900002 имеют рейтинг 1900: раньше достигший выше.
- Точный ранг дороже топа. Sorted set даёт ZREVRANK за O(log N).
- Сравнивайте Redis с индексированным SQL. В веб-данных 20 002 игрока; планы и время измеряйте на своей нагрузке.

## Slide 29. П8 Каталог в jsonb: CHECK и G
(id 191, https://easeweb.host/movie/itmo-db-lesson-3/29/)
- ПРИЛОЖЕНИЕ · П8 · POSTGRESQL · К СЛАЙДАМ 4 И 19
- [button "← к слайду 19"] -> slide 19 "Что НЕ надо переносить"
- П8. Каталог в jsonb: CHECK и GIN

```sql
SELECT id, type, stats FROM items;
SELECT id, stats->'damage' AS damage
FROM items WHERE stats @> '{"damage": 45}';
BEGIN;
SET LOCAL enable_seqscan = off; -- только демонстрация
EXPLAIN (COSTS OFF)
SELECT id FROM items WHERE stats @> '{"damage": 45}';
ROLLBACK;
INSERT INTO items VALUES
  ('potion_bad', 'Кривое зелье', 'potion', 20, '{"damage": 5}');
```
- Что должно получиться
- 3 предмета; sword_dawn | 45
  Пример плана: Bitmap Index Scan
  по items_stats_gin (после SET LOCAL).
  После ROLLBACK настройка восстановлена.
  ERROR: violates check constraint
  "potion_has_heal"; 3 строки сохранены.
- CHECK по jsonb
- GIN
- Вывод
- У зелья должен быть heal_amount и не должно быть damage.
- Оператор @> может использовать GIN. На трёх строках планировщик может предпочесть Seq Scan.
- Гибкая структура доступна и в PostgreSQL.
- Это аргумент к слайду 19. «Выбрал MongoDB, потому что гибкая структура» не засчитывается: сравнивают операции, нагрузку и цену эксплуатации.

## Slide 30. П9. Индексы MongoDB: правило ESR
(id 220, https://easeweb.host/movie/itmo-db-lesson-3/30/)
- ПРИЛОЖЕНИЕ · П9 · MONGODB · ИНДЕКСЫ · К СЛАЙДУ 7
- [button "← к слайду 7"] -> slide 7 "Referencing: вынести наружу"
- П9. Индексы MongoDB: правило ESR

```mongodb
// Веб-база: игроки уже подготовлены
db.lab_players.find({ region: "eu", level: { $gte: 20 } },
  { rating: 1, level: 1 }).sort({ rating: -1 })
db.lab_players.createIndex({ region: 1, rating: -1, level: 1 })
db.lab_players.find({ region: "eu", level: { $gte: 20 } },
  { rating: 1, level: 1 }).sort({ rating: -1 })
db.lab_players.createIndex({ tags: 1 })
db.lab_players.find({ tags: "raid" }, { rating: 1 })
db.lab_players.getIndexes()
```
- Что должно получиться
- До/после индекса: тот же результат
  по запросу (4004, затем 2001).
  Имя: region_1_rating_-1_level_1;
  tags_1 — multikey для массива.
  Веб-Run не поддерживает explain.
  В локальном mongosh сравните explain:
  COLLSCAN/IXSCAN, SORT, nReturned,
  totalKeysExamined, totalDocsExamined.
  Их значения не гарантированы ESR.
- ESR
- explain
- Multikey
- Сначала поля равенства (region), затем сортировки (rating), затем диапазона (level).
- В локальном mongosh проверьте explain("executionStats") до и после создания индекса
- Индекс по массиву хранит ключ на каждый элемент; в составном индексе массивом может быть одно поле.
- Индекс создают под запрос. Каждый индекс замедляет запись и занимает память; оставляйте только те, что использует explain реальных запросов.

## Slide 31. П10. Версия схемы и ленивая миграция
(id 221, https://easeweb.host/movie/itmo-db-lesson-3/31/)
- ПРИЛОЖЕНИЕ · П10 · MONGODB · СХЕМА · К СЛАЙДАМ 4–5
- [button "← к слайду 5"] -> slide 5 "«Схемы нет» означает «схема в"
- П10. Версия схемы и ленивая миграция

```mongodb
// Веб-база: v1 имеет nick:string и lvl:int
db.lab_profiles.find({}, { schema_version: 1 })
db.lab_profiles.updateOne({ _id: 1042, schema_version: 1,
  nick: { $type: "string" }, lvl: { $type: "int" } },
  [{ $set: { nickname: "$nick", level: "$lvl",
             region: "unknown", schema_version: 2 } },
   { $unset: ["nick", "lvl"] }])
db.lab_profiles.findOne({ _id: 1042 })
db.lab_profiles.updateOne({ _id: 1042, schema_version: 1 },
  [{ $set: { schema_version: 2 } }])
db.lab_profiles.countDocuments({ schema_version: { $lt: 2 } })
```
- Что должно получиться

```sql
1042: v1, 2001: v1, 3003: v2
matchedCount: 1, modifiedCount: 1
{ _id: 1042, schema_version: 2,
  nickname: 'Klinok', level: 13,
  region: 'unknown' }
matchedCount: 0 // повтор не сработал
1              // осталась 2001 в v1
```
- schema_version
- Ленивая миграция
- Пакетная миграция
- Номер формы документа. Код читает все живые версии, пока старые не мигрированы.
- В коде — миграция при явном updateOne. «Ленивая» требует вызова при чтении/записи в приложении.
- Перед updateMany: фильтр version=1, проверка nick/lvl и допустимости;
  затем аудит v2, лишь после validator.
- Гибкая схема это несколько форм документа одновременно. Миграции не исчезают: они становятся постепенными, и за них отвечает код.

## Slide 32. П11. Надёжная очередь: Redis Streams
(id 222, https://easeweb.host/movie/itmo-db-lesson-3/32/)
- ПРИЛОЖЕНИЕ · П11 · REDIS · ОЧЕРЕДЬ · К СЛАЙДУ 13
- [button "← к слайду 13"] -> slide 13 "Типы значений и пути доступа"
- П11. Надёжная очередь: Redis Streams

```redis
# очередь матчмейкинга на Stream
DEL lesson03:mm:queue
XADD lesson03:mm:queue 1-1 player_id 1042 mode ranked
XADD lesson03:mm:queue 1-2 player_id 2001 mode ranked
XGROUP CREATE lesson03:mm:queue matchers 0
XREADGROUP GROUP matchers worker-1 COUNT 10 STREAMS lesson03:mm:queue >
XACK lesson03:mm:queue matchers 1-1
XPENDING lesson03:mm:queue matchers
XAUTOCLAIM lesson03:mm:queue matchers worker-2 0 0-0 COUNT 10
XACK lesson03:mm:queue matchers 1-2
XPENDING lesson03:mm:queue matchers
```
- Что должно получиться
- "1-1" "1-2" OK
  worker-1 получил 1-1 и 1-2
  (integer) 1 // 1-1 подтверждено
  1) 1 2) "1-2" … // 1-2 ещё в работе
  // у worker-1
  worker-2 забрал 1-2 // XAUTOCLAIM
  (integer) 1
  1) 0 … // ничего не висит
- Consumer group
- XACK
- XAUTOCLAIM
- Каждое сообщение получает один обработчик группы; группа помнит, что выдано.
- XPENDING хранит статус до XACK; сбой обработчика допускает повтор. Без RDB/AOF рестарт Redis теряет поток.
- После сбоя worker возможна повторная доставка; обработка должна быть идемпотентной. Это не защита от потери Redis.
- XACK удаляет pending-ссылку, не запись потока. В стенде нет RDB/AOF; нужна persistence и политика удержания: MAXLEN может убрать тело ещё не подтверждённого сообщения.

## Slide 33. П12. Идемпотентность и блокировка: SET NX
(id 223, https://easeweb.host/movie/itmo-db-lesson-3/33/)
- ПРИЛОЖЕНИЕ · П12 · REDIS · ПОВТОРЫ · К СЛАЙДУ 16
- [button "← к слайду 16"] -> slide 16 "Лидерборд на sorted set"
- П12. Идемпотентность и блокировка: SET NX

```redis
# повтор события и замок
DEL lesson03:match:777:applied lesson03:lb lesson03:lock:mm:1042
SET lesson03:match:777:applied 1 NX EX 86400
ZINCRBY lesson03:lb 25 player:1042
SET lesson03:match:777:applied 1 NX EX 86400
ZSCORE lesson03:lb player:1042
SET lesson03:lock:mm:1042 worker-1 NX PX 5000
SET lesson03:lock:mm:1042 worker-2 NX PX 5000
GET lesson03:lock:mm:1042
```
- Что должно получиться
- (integer) 0
  OK // матч 777 применяем
  "25"
  (nil) // повтор: не применяем
  "25" // очки не удвоились
  OK // замок у worker-1
  (nil) // worker-2 ждёт
  "worker-1"
- Метка события
- Не атомарно
- Замок с TTL
- SET NX показывает только повтор без сбоя. Срок метки должен покрывать всё окно повторов; потеря метки даст дубль.
- Метка + ZINCRBY здесь не атомарны: сбой оставит метку без очков. Нужны единый Lua-скрипт или PG-дедупликация.
- PX ограничивает срок, но после истечения второй worker может войти. Удалять замок — только атомарно по токену.
- Это демонстрация ловушки, не готовая идемпотентность. При истечении/потере метки очки можно начислить повторно. Для денег — durable дедупликация в PostgreSQL.

## Slide 34. П13. Лавина промахов: разброс TTL и замок
(id 224, https://easeweb.host/movie/itmo-db-lesson-3/34/)
- ПРИЛОЖЕНИЕ · П13 · REDIS · КЭШ · К СЛАЙДУ 15
- [button "← к слайду 15"] -> slide 15 "TTL — не гарантия свежести"
- П13. Лавина промахов: разброс TTL и замок

```redis
# Веб-данные; пример cache miss и двух претендентов
DEL lesson03:profile:1042 lesson03:rebuild:profile:1042
GET lesson03:profile:1042
SET lesson03:rebuild:profile:1042 worker-1 NX PX 3000
SET lesson03:rebuild:profile:1042 worker-2 NX PX 3000
# приложение выбирает TTL случайно: здесь 312 = 300 + 12
SET lesson03:profile:1042 level=14 EX 312
GET lesson03:profile:1042
# после работы: release только по токену, атомарно
```
- Что должно получиться
- DEL: число удалённых ключей зависит
  от прошлого запуска.
  GET: (nil) — кэш пуст
  SET NX: OK, затем (nil)
  SET cache: OK; GET: "level=14"
  312 выбран как пример: блок сам
  случайный разброс не генерирует.
- Разброс TTL
- Один перестроитель
- Остальные
- В приложении при записи выбирают новый TTL: 300 с + случайные 0–60; 312 здесь только пример.
- При промахе только владелец замка строит кэш. NX не защищает от записи старого результата после истечения PX.
- Остальные ждут или отдают допустимо старое. Замок снимают атомарно по токену; нужны версия/проверка свежести записи.
- Промах по популярному ключу = тысячи одинаковых запросов в PostgreSQL. Ограничивайте перестройку, а не только срок жизни. Вариант: обновлять ключ заранее, до истечения.

## Slide 35. П14. Один поток команд и большие ключи
(id 225, https://easeweb.host/movie/itmo-db-lesson-3/35/)
- ПРИЛОЖЕНИЕ · П14 · REDIS · ПРОИЗВОДИТЕЛЬНОСТЬ · К СЛАЙДАМ 14 И 17
- [button "← к слайду 14"] -> slide 14 "Семейства ключей — часть схемы"
- П14. Один поток команд и большие ключи

```redis
# большой ключ: размер, затем порциями
DEL lesson03:big:hash
HSET lesson03:big:hash f1 a f2 b f3 c f4 d
HLEN lesson03:big:hash
HSCAN lesson03:big:hash 0 COUNT 2
ZCARD leaderboard:season:7
ZRANGE leaderboard:season:7 0 1 REV WITHSCORES
UNLINK lesson03:big:hash
```
- Что должно получиться
- (integer) 0
  (integer) 4
  (integer) 4 // размер — до чтения
  1) "0" 2) f1 a … f4 d
  // маленький хеш: всё сразу,
  // COUNT — только подсказка
  (integer) 3
  1) "player:2001" 2) "1720" …
  (integer) 1 // удалён в фоне
  На стенде: MEMORY USAGE key,
  redis-cli --bigkeys, SLOWLOG GET 10
- Один поток
- Порциями
- UNLINK
- Команды выполняются по одной: HGETALL на миллион полей задерживает всех клиентов.
- Сначала HLEN / ZCARD, затем HSCAN / SSCAN / ZSCAN или диапазон ZRANGE 0 99 вместо чтения целиком.
- Освобождает память большого значения в фоновом потоке; DEL делает это сразу и блокирует.
- Опасен не только KEYS: любая O(N)-команда на большом ключе. Большие ключи дробят: inventory на игрока, а не один хеш на всех; сезонный рейтинг — отдельным ключом на сезон.

## Slide 36. П15. Transactional outbox: событие до доставки
(id 226, https://easeweb.host/movie/itmo-db-lesson-3/36/)
- ПРИЛОЖЕНИЕ · П15 · POSTGRESQL · СИНХРОНИЗАЦИЯ · К СЛАЙДАМ 18–19
- [button "← к слайду 18"] -> slide 18 "Недоступно ≠ потеряно"
- П15. Transactional outbox: событие до доставки

```sql
-- веб-база подготовлена; после Reset: level 13, outbox пуст
BEGIN;
UPDATE player_profiles SET level = level + 1 WHERE player_id = 1042;
INSERT INTO outbox (topic, payload)
VALUES ('profile.changed', '{"player_id": 1042}');
COMMIT;
-- событие ЕЩЁ не доставлено в Redis
SELECT id, topic, payload FROM outbox
WHERE sent_at IS NULL ORDER BY id LIMIT 100;
SELECT count(*) AS unsent FROM outbox WHERE sent_at IS NULL;
-- worker: DEL в Redis; sent_at только после успеха
```
- Что должно получиться
- После Reset: 1042 | 14, 2001 | 7
  id 1 | profile.changed | {"player_id":1042}
  unsent = 1 (до успешной доставки)
  Worker: DEL player:1042:profile
  в Redis; после успеха — отметка sent_at.
  Сбой до отметки: повтор; DEL идемпотентен.
  Повтор Run без Reset добавит событие.
- Двойная запись
- Outbox
- Обработчик
- Commit в PG, затем DEL в Redis: упали между ними — кэш устарел до TTL, и никто не знает.
- Событие пишется в той же транзакции, что изменение: либо оба, либо ни одного.
- Резервирует событие (lease/короткая транзакция), делает DEL, только после успеха ставит sent_at; сбой — повтор.
- Общего commit между PostgreSQL и Redis нет. Пока событие в PG не отмечено, worker может повторить DEL. Нужны мониторинг lag и сохранность PG.

## Slide 37. П16. HyperLogLog и битовые карты
(id 227, https://easeweb.host/movie/itmo-db-lesson-3/37/)
- ПРИЛОЖЕНИЕ · П16 · REDIS · АНАЛИТИКА · К СЛАЙДУ 13
- [button "← к слайду 13"] -> slide 13 "Типы значений и пути доступа"
- П16. HyperLogLog и битовые карты

```redis
# уникальные за день и дни входа
DEL lesson03:dau:2026-09-29 lesson03:dau:2026-09-30 lesson03:login:1042:2026-09
PFADD lesson03:dau:2026-09-29 1042 2001 3003
PFADD lesson03:dau:2026-09-30 1042 2001 1042 4004
PFCOUNT lesson03:dau:2026-09-30
PFCOUNT lesson03:dau:2026-09-29 lesson03:dau:2026-09-30
SETBIT lesson03:login:1042:2026-09 28 1
SETBIT lesson03:login:1042:2026-09 29 1
BITCOUNT lesson03:login:1042:2026-09
GETBIT lesson03:login:1042:2026-09 27
```
- Что должно получиться
- PFADD: (integer) 1, затем 1
  PFCOUNT дня: примерно 3
  PFCOUNT двух дней: примерно 4
  Оценка, не гарантия точного числа.
  SETBIT: (integer) 0, затем 0
  BITCOUNT: (integer) 2
  GETBIT для 28.09: (integer) 0
- HyperLogLog
- Объединение
- Битовая карта
- Приблизительное число уникальных: ошибка около 0,81 %, не больше 12 КБ на ключ. Список ID не хранит.
- PFCOUNT по нескольким ключам оценивает уникальных за период без сложения дневных чисел; результат приблизительный.
- 30/31 бит требуют 4 байта данных на игрока; размер Redis-ключа и накладные расходы — дополнительно.
- Точные деньги и достижения — в PostgreSQL. Приблизительная аналитика и счётчики активности — хороший кандидат для Redis или аналитической СУБД.

## Slide 38. П17. Redis и MongoDB не открывают в интернет
(id 228, https://easeweb.host/movie/itmo-db-lesson-3/38/)
- ПРИЛОЖЕНИЕ · П17 · БЕЗОПАСНОСТЬ · К СЛАЙДАМ 17–18
- [button "← к слайду 17"] -> slide 17 "Память: лимит и политику выбир"
- П17. Redis и MongoDB не открывают в интернет

```bash
# Пример, не текущий Compose; адреса заменить на свои
# redis.conf
bind 127.0.0.1 10.0.0.5
protected-mode yes
aclfile /etc/redis/users.acl
# users.acl (одна строка на пользователя, вне репо)
user default off
user profiles on >REPLACE_ME ~player:* ~session:* +@read +@write -@dangerous +ping
# mongod.conf (предварительно создать admin)
net:
  bindIp: 127.0.0.1,10.0.0.6
security:
  authorization: enabled
```
- Как проверить
- Внешний IP: соединения нет (при закрытом порте).
  Без AUTH на 10.0.0.5: NOAUTH
  только после user default off.
  AUTH profiles <секрет> → PING;
  разрешённые ключи доступны.
  Mongo без входа: Unauthorized
  после создания admin и включения auth.
  Compose стенда: internal, нет портов;
  пароли в этом стенде не включены.
- Не в интернет
- Минимум прав
- Секреты
- Открытые Redis и MongoDB находят сканеры; данные удаляют и требуют выкуп.
- ACL и роли: сервису профилей не нужны FLUSHALL, CONFIG, KEYS и чужие ключи.
- REPLACE_ME — только заглушка; настоящий пароль — в ACL-файле вне репозитория/хранилище секретов. Нужен TLS.
- Стенд защищён сетью Docker, а не паролем. Для проекта минимум: доступ только из внутренней сети, пароль или ACL в Redis, authorization в MongoDB.
