DynamoDB — это управляемая база данных типа NoSQL от AWS. «Управляемая» значит, что вам не нужно ставить сервер, обновлять его и думать про репликацию: AWS делает это за вас. «NoSQL» значит, что это не привычная таблица со строками и связями, где можно писать произвольные SQL-запросы, а хранилище типа «ключ-значение»: вы кладёте элемент по ключу и забираете его по тому же ключу. Главная особенность DynamoDB — он держит стабильно низкую задержку (единицы миллисекунд) хоть на тысяче записей, хоть на миллиардах. И один жёсткий предел, который определяет всю модель данных: один элемент не может быть больше 400 КБ. Всё крупное — файлы, изображения, длинные документы — уезжает в объектное хранилище, а в таблице остаётся ссылка.

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

Ключи: partition key и sort key

У каждого элемента (так в DynamoDB называют запись, аналог строки) есть первичный ключ. Он состоит из одной или двух частей.

Partition key (ключ партиции) — обязательная часть. По нему DynamoDB решает, на каком физическом «куске» хранилища (партиции) будет лежать элемент. Представьте библиотеку с множеством стеллажей: partition key — это номер стеллажа. От него зависит, насколько ровно распределится нагрузка. Плохой ключ — например, у которого мало разных значений или один встречается чаще остальных — создаёт «горячую» партицию: один стеллаж осаждён читателями, остальные пустуют, и он становится бутылочным горлышком.

У «осаждённого стеллажа» есть и точные числа: одна партиция отдаёт примерно 3000 единиц чтения и 1000 единиц записи в секунду — в них всё и упирается. Часть перекоса DynamoDB сглаживает сам (это называют адаптивной ёмкостью: горячему ключу подкидывают пропускную способность за счёт спокойных соседей), но спасает это не всегда, поэтому ключ всё равно подбирают так, чтобы значений было много и нагрузка по ним ложилась ровно.

Sort key (ключ сортировки) — необязательная вторая часть. Если partition key — это номер стеллажа, то sort key — это порядок книг на полке. Он упорядочивает элементы внутри одной партиции и позволяет делать запросы по диапазону: «все заказы пользователя за март», «всё, что начинается с ORDER#». Для этого есть операторы вроде begins_with (начинается с) и between (в диапазоне).

Главная операция чтения — Query. Она работает быстро, но только по ключу: «дай элементы с таким partition key и sort key в таком диапазоне». Всё, что не по ключу, делается через Scan — полный перебор всей таблицы. Scan дорогой и медленный, его избегают. Отсюда правило новичка: сперва выпиши все запросы приложения, потом подбери ключи так, чтобы каждый запрос попадал в Query.

Отдельно стоит сказать про FilterExpression — он есть и у Query, и у Scan, и он обманчив. Фильтр применяется уже после того, как данные прочитаны, поэтому платите вы за всё прочитанное, а не за то, что осталось после фильтра, и время тратится тоже на всё. «Я же отфильтровал» — ошибка номер один у начинающих.

aws dynamodb query \
  --table-name Orders \
  --key-condition-expression "pk = :u AND begins_with(sk, :prefix)" \
  --expression-attribute-values '{":u":{"S":"USER#42"},":prefix":{"S":"ORDER#"}}'

Индексы: GSI и LSI

Часто нужно искать данные не только по основному ключу. Например, заказы хранятся по USER#42, но иногда надо найти заказ по его статусу. Для таких дополнительных способов поиска заводят вторичные индексы — это автоматически поддерживаемые копии данных с другим ключом.

GSI (Global Secondary Index, глобальный вторичный индекс) — отдельный индекс со своим partition key и sort key. У него своя пропускная способность, не зависящая от таблицы. GSI можно добавить в любой момент жизни таблицы — это удобно, когда появился новый сценарий поиска. Важное ограничение: чтение из GSI всегда eventually consistent (про это ниже), и это закладывают в дизайн.

LSI (Local Secondary Index, локальный вторичный индекс) — индекс с тем же partition key, что у таблицы, но другим sort key. У него три отличия от GSI, и у каждого есть последствие:

  • создаётся только в момент создания таблицы, потом не добавить и не удалить — значит, решение принимается один раз, при проектировании;
  • делит пропускную способность с таблицей — тяжёлые запросы по индексу замедляют саму таблицу;
  • для одного значения partition key суммарный объём данных вместе с LSI ограничен 10 ГБ — «горячий» ключ упрётся в потолок.

Зато LSI умеет отдавать strongly consistent чтения.

aws dynamodb create-table \
  --table-name Orders \
  --attribute-definitions AttributeName=pk,AttributeType=S AttributeName=status,AttributeType=S \
  --key-schema AttributeName=pk,KeyType=HASH \
  --global-secondary-indexes '[{"IndexName":"by-status","KeySchema":[{"AttributeName":"status","KeyType":"HASH"}],"Projection":{"ProjectionType":"ALL"}}]' \
  --billing-mode PAY_PER_REQUEST

На практике GSI — основной инструмент для дополнительных способов доступа, потому что его гибче добавить и масштабировать. Каждый индекс — это копия данных (можно выбрать, какие атрибуты в неё проецировать), за которую вы платите местом и записями. Поэтому индексы заводят строго под конкретный запрос, а не «вдруг пригодится».

Режимы оплаты: on-demand и provisioned

DynamoDB берёт деньги за пропускную способность — за чтения и записи. Есть два режима.

On-demand (по требованию, PAY_PER_REQUEST) — вы платите за каждый запрос по факту, а ёмкость подстраивается под нагрузку сама. Это самый простой старт: ничего не настраиваешь, не угадываешь объём. Подходит для нового проекта и для неровной, скачущей нагрузки. Одна оговорка про «сама»: без запинки таблица примет вдвое больше вашего прежнего пика, а рост сверх этого стоит растянуть примерно на полчаса — иначе внезапный десятикратный всплеск упрётся в отказы по превышению (throttling).

Provisioned (зарезервированный) — вы заранее задаёте количество единиц чтения и записи в секунду (можно включить авто-масштабирование, чтобы они менялись по графику). При стабильной предсказуемой нагрузке это может выйти дешевле, потому что вы платите за зарезервированное, а не за каждый запрос.

Совет «начните с on-demand, а когда нагрузка устаканится — переезжайте на provisioned ради экономии» долгие годы был стандартным, но в конце 2024 года AWS вдвое снизил цену on-demand, и разрыв сильно сократился. Сейчас on-demand — режим по умолчанию и рекомендованный, так что считайте на своих числах: экономия от provisioned стала меньше, а возни с ней больше. Подробнее про подсчёт денег в облаке — в материале про оптимизацию затрат.

Консистентность: насколько свежие данные вы видите

Консистентность — это про то, насколько свежие данные вы видите сразу после записи. В DynamoDB чтение по умолчанию eventually consistent (согласованность в конечном счёте): оно дешевле, но сразу после записи может на доли секунды вернуть чуть устаревшее значение, пока изменение разойдётся по копиям. Если нужна гарантия самых свежих данных, явно просят strongly consistent чтение — оно дороже и чуть медленнее. Запомните: чтение из GSI всегда только eventually consistent, выбора там нет.

Single-table design: приём не для старта

Single-table design — продвинутый приём, когда разные типы сущностей (например, заказы и их позиции) кладут в одну таблицу, чтобы одним запросом достать связанные данные сразу. Это мощно и экономит обращения к базе, но проектировать такое сложно. Для многих сервисов проще и понятнее держать несколько отдельных таблиц. Это осознанный выбор под нагрузку, а не обязательная практика — начинающему почти всегда стоит начать с нескольких простых таблиц.

Когда DynamoDB, а когда реляционная база

Главная развилка, которую важно проговорить — DynamoDB и реляционная база (RDS) решают разные классы задач, ни одна не «лучше».

DynamoDB хорош, когда способы доступа известны заранее и стабильны, запросы простые и идут по ключу, а масштаб большой при требовании предсказуемой задержки. Типичные примеры: пользовательские сессии, профили, история событий, корзины покупок, лента уведомлений.

RDS (PostgreSQL, MySQL и подобные) нужен, когда запросы сложные: объединения нескольких таблиц (join), агрегаты и отчёты, и ситуации, где способ доступа к данным со временем меняется и хочется гибкости SQL. Если вы пока не знаете, какие запросы понадобятся, реляционная база прощает это лучше.

Частое заблуждение — что транзакций в DynamoDB нет. Они есть: TransactWriteItems и TransactGetItems проводят до 100 элементов по принципу «всё или ничего», в том числе в разных таблицах одного аккаунта и региона. Граница проходит не по их наличию, а по рамкам и цене: сотня элементов, суммарно 4 МБ, и каждая операция внутри транзакции стоит вдвое дороже обычной. Если бизнес-операция регулярно трогает десятки сущностей сразу, в реляционной базе это будет проще и дешевле.

Простое правило: знаете запросы наперёд и их немного, нужен масштаб — берите DynamoDB и проектируйте ключи под запросы; нужна гибкость и сложные связи — берите реляционную базу.

Где это применяется

DynamoDB встречается почти в любом приложении на AWS, где есть «горячие» данные с понятным доступом по ключу и высокой нагрузкой: хранение сессий, профилей, флагов функций, корзин, лент событий. Особенно естественно он ложится в serverless-архитектуры на Lambda, где сервер не держат постоянно, а база должна масштабироваться сама и оплачиваться по факту.

Типичные ошибки начинающих:

  • спроектировать таблицу «как в SQL» и потом упереться в Scan на каждом экране;
  • выбрать partition key с малым числом значений и получить горячую партицию;
  • добавить кучу GSI «на всякий случай» и платить за них;
  • забыть, что LSI нельзя добавить после создания таблицы;
  • ждать от GSI strongly consistent чтения, которого там нет;
  • сразу полезть в single-table design без реальной потребности.

Что учить дальше: разберите соседние управляемые базы данных AWS, чтобы видеть всю палитру хранилищ; пройдите основы AWS и serverless, где DynamoDB используется чаще всего; загляните в объектное хранилище — это другой тип данных (файлы, бэкапы) и удобно понимать границу между ними. Полезно также посмотреть хорошо спроектированную архитектуру AWS — там выбор хранилища под задачу разобран как часть инженерных принципов.