AWS

Amazon DynamoDB — NoSQL Database

Фундаментальний посібник з Amazon DynamoDB для .NET розробників. Від моделі даних та первинних ключів до вторинних індексів, транзакцій, Streams, TTL, Global Tables та інтеграції через AWS SDK for .NET — з акцентом на проектування схем для реальних production-сценаріїв.

Amazon DynamoDB — NoSQL Database

Реляційна парадигма та її межі: навіщо існує DynamoDB

Протягом більш ніж чотирьох десятиліть реляційна модель баз даних домінувала у сфері зберігання структурованих даних. PostgreSQL, MySQL, SQL Server — ці системи побудовані на фундаменті, що залишається незмінним з часів Едгара Кодда: дані організовані у таблиці зі строго визначеною схемою, зв'язки між сутностями встановлюються через зовнішні ключі, а мова запитів SQL надає декларативний інтерфейс для маніпуляції даними. Для абсолютної більшості корпоративних застосунків ця модель є не просто достатньою — вона є оптимальною.

Однак у 2000-х роках провідні інтернет-компанії зіткнулися зі сценарієм, де реляційна модель показала системні обмеження. Йдеться не про невелику деградацію продуктивності — йдеться про принципову неможливість масштабування до потрібних обсягів при збереженні прийнятних затримок. Проблема полягає у фундаментальній властивості реляційних систем: для гарантування ACID-властивостей (передусім Consistency та Isolation) вони використовують механізми блокувань, які за своєю природою є централізованими. Єдиний вузол або невеликий кластер може забезпечити сильну узгодженість, але горизонтальне масштабування на тисячі вузлів цю гарантію руйнує.

Команда Amazon у 2004–2007 роках дослідила власні внутрішні сервіси та виявила закономірність: 70% операцій на їхніх production-системах мали форму "прочитати або записати одну сутність за відомим ідентифікатором", і лише 20% дійсно потребували можливостей JOIN та складних SQL-запитів. Це спостереження лягло в основу фундаментальної роботи Dynamo: Amazon's Highly Available Key-Value Store (2007, Werner Vogels та ін.), яка описала архітектуру системи, що жертвує частиною семантики реляційної моделі заради лінійного горизонтального масштабування та гарантованих затримок.

Amazon DynamoDB — це хмарний керований NoSQL сервіс AWS, що є еволюцією оригінальної Dynamo-системи, публічно запущений у 2012 році. DynamoDB надає три гарантії, які в поєднанні недосяжні для будь-якої реляційної СУБД при масштабуванні:

  1. Продуктивність з однозначними мілісекундами — читання та запис будь-якого розміру таблиці виконуються з затримкою 1–10 ms (при правильному проектуванні)
  2. Необмежений горизонтальний масштаб — одна таблиця DynamoDB здатна зберігати петабайти даних і обробляти мільйони запитів на секунду
  3. Повністю керований сервіс — відсутність необхідності управляти серверами, patching, бекапами, реплікацією
DynamoDB — це не заміна PostgreSQL. Це інший інструмент для інших сценаріїв. У межах цього курсу ми розглянемо обидві системи: RDS PostgreSQL для транзакційних даних з комплексними відносинами, DynamoDB — для високонавантажених сценаріїв з передбачуваними патернами доступу. Вибір між ними є архітектурним рішенням, а не питанням переваг.
Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

rectangle "Реляційна модель (RDS PostgreSQL)" as RDB #dbeafe {
    note as RDB_NOTE
      ✅ Складні JOIN між таблицями
      ✅ Транзакції через кілька таблиць
      ✅ Гнучкі ad-hoc запити (SQL)
      ✅ Сильна узгодженість (ACID)
      ❌ Горизонтальне масштабування складне
      ❌ Латентність зростає з обсягом даних
      ❌ Schema migrations при змінах
    end note
}

rectangle "NoSQL модель (DynamoDB)" as NOSQL #d1fae5 {
    note as NOSQL_NOTE
      ✅ Необмежений горизонтальний масштаб
      ✅ Гарантована низька латентність (~1–10ms)
      ✅ Гнучка схема (schema-less)
      ✅ Автоматична реплікація та HA
      ❌ Не підтримує JOIN між таблицями
      ❌ Обмежені можливості запитів
      ❌ Потребує ретельного проектування ключів
    end note
}

rectangle "Коли обирати RDS" as WHEN_RDB #eff6ff {
    note as WHEN_RDB_NOTE
      - Корпоративні системи з комплексними
        зв'язками (ERP, CRM, облік)
      - Фінансові транзакції між таблицями
      - Аналітичні запити (GROUP BY, aggregate)
      - Команда добре знає SQL
    end note
}

rectangle "Коли обирати DynamoDB" as WHEN_NOSQL #f0fdf4 {
    note as WHEN_NOSQL_NOTE
      - Сесії користувачів, кешування
      - Ігрові leaderboards
      - IoT-дані (часові ряди)
      - E-commerce (кошики, каталог товарів)
      - Мільйони запитів/сек з <10ms
    end note
}

RDB -[hidden]right-> NOSQL
WHEN_RDB -[hidden]right-> WHEN_NOSQL

@enduml

Модель даних DynamoDB: таблиці, елементи та атрибути

Таблиця (Table)

Таблиця (Table) є найвищим рівнем організації даних у DynamoDB. На перший погляд, таблиця DynamoDB схожа на таблицю реляційної бази даних — вона має назву та містить записи. Проте між ними існує принципова різниця у семантиці.

У реляційній моделі таблиця визначає схему: структуру кожного рядка з іменами стовпців та їхніми типами задають при CREATE TABLE, і всі рядки повинні відповідати цій схемі. DynamoDB, навпаки, є schema-less на рівні атрибутів: єдине, що таблиця DynamoDB вимагає від кожного елемента — наявність атрибутів, що складають первинний ключ. Решта атрибутів кожного елемента може бути абсолютно довільною та різною від елемента до елемента.

Таблиця DynamoDB є незалежним ресурсом — вона не "належить" схемі чи базі даних. Якщо в PostgreSQL ви маєте database → schema → table, то у DynamoDB ієрархія скорочується до table. Це означає, що таблиці DynamoDB не можуть бути об'єднані через JOIN навіть синтаксично — такого механізму не існує.

Елемент (Item)

Елемент (Item) — це одиниця даних у таблиці DynamoDB, аналог рядка (row) у реляційній базі даних. Кожен елемент унікально ідентифікується своїм первинним ключем та є колекцією атрибутів. Максимальний розмір одного елемента — 400 KB (разом з іменами атрибутів та значеннями).

Відсутність фіксованої схеми надає DynamoDB надзвичайну гнучкість при еволюції моделі даних. Уявіть таблицю Products, де різні категорії товарів мають різний набір атрибутів:

// Елемент 1: Книга
{
    "ProductId": "book-001",
    "Category": "Book",
    "Title": "Clean Code",
    "Author": "Robert C. Martin",
    "ISBN": "978-0132350884",
    "Pages": 431,
    "Price": 35.00
}

// Елемент 2: Електроніка
{
    "ProductId": "elec-002",
    "Category": "Electronics",
    "Title": "Sony WH-1000XM5",
    "Brand": "Sony",
    "WarrantyYears": 2,
    "BatteryLifeHours": 30,
    "Price": 349.99
}

Обидва елементи належать одній таблиці, але мають абсолютно різні набори атрибутів. У PostgreSQL для цього знадобилося б або успадкування таблиць, або поле типу JSONB, або окремі таблиці для кожної категорії.

Атрибут (Attribute) та система типів

Атрибут (Attribute) — це пара "ім'я–значення", що належить конкретному елементу. DynamoDB підтримує систему типів даних, яка суттєво відрізняється від SQL-типів:

Скалярні типи (Scalar)

String (S) — рядок у кодуванні UTF-8. Порожній рядок ("") дозволений.

Number (N) — числове значення (ціле або дробове). Зберігається як рядок для збереження точності. Діапазон: до 38 значущих цифр.

Binary (B) — бінарні дані у форматі Base64. Використовується для зберігання зображень, стиснутих даних, криптографічних хешів.

Boolean (BOOL)true або false.

Null (NULL) — відсутність значення. Атрибут типу Null займає місце, але не має значення.

Документні типи (Document)

Map (M) — довільна структура вигляду "ключ–значення", аналог JSON-об'єкта. Значення кожного ключа може бути будь-яким типом DynamoDB, включаючи вкладені Map.

List (L) — впорядкована колекція значень довільних типів. Аналог JSON-масиву. Елементи списку можуть бути різних типів.

Множинні типи (Set)

StringSet (SS) — невпорядкована колекція унікальних рядків.

NumberSet (NS) — невпорядкована колекція унікальних чисел.

BinarySet (BS) — невпорядкована колекція унікальних бінарних значень.

Важливо: Set не може бути порожнім. Всі елементи Set мають бути одного типу.

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

package "DynamoDB Type System" {
    rectangle "Scalar Types" as SCALAR #fef3c7 {
        note as S_NOTE
          String (S):     "London"
          Number (N):     42.5
          Binary (B):     <base64>
          Boolean (BOOL): true
          Null (NULL):    null
        end note
    }

    rectangle "Document Types" as DOC #dbeafe {
        note as D_NOTE
          Map (M): {
            "city": "London",
            "zip": "EC1A"
          }
          List (L): [
            "one", 2, true, {"key": "val"}
          ]
        end note
    }

    rectangle "Set Types" as SETS #d1fae5 {
        note as SET_NOTE
          StringSet (SS):
            ["red", "green", "blue"]

          NumberSet (NS):
            [1, 2, 3, 5, 8, 13]

          BinarySet (BS):
            [<b64_1>, <b64_2>]
        end note
    }
}

@enduml

Приклад елемента з вкладеними типами:

{
    "UserId": "usr-123",
    "Name": "Олена Петренко",
    "Email": "elena@example.com",
    "Age": 28,
    "IsActive": true,
    "Tags": ["developer", "aws-certified", "ukraine"],
    "Address": {
        "Country": "Ukraine",
        "City": "Kyiv",
        "ZipCode": "01001"
    },
    "Skills": ["C#", ".NET", "DynamoDB", "PostgreSQL"],
    "LastLoginAt": "2025-06-01T10:30:00Z"
}

У цьому прикладі:

  • UserId, Name, Email, LastLoginAt — тип String (S)
  • Age — тип Number (N)
  • IsActive — тип Boolean (BOOL)
  • Tags — тип List (L) зі String-елементами
  • Address — тип Map (M) з вкладеними String-атрибутами
  • Skills — тип StringSet (SS) (множина, не може містити дублікати)

Первинний ключ (Primary Key): фундамент DynamoDB

Первинний ключ є найважливішим концептуальним елементом DynamoDB. На відміну від реляційних баз даних, де первинний ключ є лише засобом унікальної ідентифікації рядка, у DynamoDB первинний ключ виконує дві нерозривно пов'язані функції одночасно: унікальну ідентифікацію елемента та визначення фізичного розташування даних на серверах. Саме тому вибір первинного ключа є фундаментальним архітектурним рішенням, від якого залежить продуктивність системи при будь-якому масштабі.

DynamoDB підтримує два типи первинного ключа.

Partition Key (Simple Primary Key)

Partition Key (також відомий як Hash Key або PK) — це простий первинний ключ, що складається з одного атрибута. DynamoDB використовує значення Partition Key як вхідні дані для детермінованої хеш-функції, результат якої визначає, на якому фізичному сервері (partition) зберігатиметься елемент.

Візуалізація Simple Primary Key

Приклад 1: Таблиця Users (Профілі користувачів)

У цій таблиці первинним ключем є UserId (тип String). Всі інші атрибути (Name, Age, Email) є довільними та необов'язковими для схеми:

UserId (PK)
тип: String (S)
Name
тип: String (S)
Age
тип: Number (N)
Email
тип: String (S)
usr-001Олена28elena@example.com
usr-002Іван32ivan@example.com
usr-003Марія22maria@example.com
Приклад 2: Таблиця Devices (Метадані IoT-пристроїв)

У цьому сценарії первинним ключем є DeviceId (тип String), який ідентифікує конкретний фізичний пристрій. Кожен пристрій має свій набір налаштувань та метаданих:

DeviceId (PK)
тип: String (S)
DeviceType
тип: String (S)
FirmwareVersion
тип: String (S)
Status
тип: String (S)
Metadata
тип: Map (M)
dev-sensor-091Temperaturev2.4.1ACTIVE{"Location": "ServerRoom-A", "IntervalSec": 10}
dev-camera-104IP-Camerav1.0.8OFFLINE{"Location": "MainEntrance", "Resolution": "4K"}
dev-gate-002SmartGatev3.1.0ACTIVE{"Location": "GateEast"}

Основні особливості роботи з простим ключем:

  • Унікальність: Кожен запис (елемент) у таблиці повинен мати унікальне значення UserId. Запис елемента з наявним UserId перезапише старий елемент.
  • Швидкий доступ: Доступ до даних здійснюється виключно за точним значенням Partition Key (наприклад, GetItem за UserId = "usr-001"). DynamoDB обчислює хеш від ключа та миттєво знаходить потрібну партицію.
  • Обмеження вибірок: Неможливо зробити запит (Query) на кшталт "показати всіх користувачів, які старші за 25 років". Подібна операція без додаткових індексів вимагає сканування всієї таблиці (Scan), що є дорогою операцією та створює велике навантаження.

Нижче показано, як DynamoDB розподіляє ці дані на фізичному рівні:

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

title "Partition Key → фізичний розподіл даних"

rectangle "Таблиця: Users" as TBL #fef3c7

together {
    card "Item\nUserId: usr-001\nName: Олена" as I1 #bbf7d0
    card "Item\nUserId: usr-002\nName: Іван" as I2 #bbf7d0
    card "Item\nUserId: usr-003\nName: Марія" as I3 #bbf7d0
}

TBL --> I1
TBL --> I2
TBL --> I3

rectangle "Hash Function\nhash(\"usr-001\") → 0x3A2F...\nhash(\"usr-002\") → 0x8B1C...\nhash(\"usr-003\") → 0x1D47..." as HASH #e0e7ff

I1 --> HASH
I2 --> HASH
I3 --> HASH

package "Фізичні партиції" as PARTITIONS {
    node "Partition A\n[0x0000..0x5FFF]" as PA #d1fae5
    node "Partition B\n[0x6000..0xAFFF]" as PB #d1fae5
    node "Partition C\n[0xB000..0xFFFF]" as PC #d1fae5
}

HASH --> PA : "usr-001, usr-003\n(хеш попав у діапазон A, C)"
HASH --> PB : "usr-002\n(хеш у діапазоні B)"

note bottom of PA
  Кожна партиція —
  окремий SSD-диск
  на окремому сервері AWS.
  Максимум 10 GB даних
  та 3000 RCU / 1000 WCU.
end note

@enduml

Критична вимога до Partition Key: висока кардинальність. Якщо всі або більшість елементів мають однакове значення Partition Key, всі вони потраплять на одну фізичну партицію. Ця ситуація називається hot partition (гаряча партиція) і є найчастішою причиною деградації продуктивності у DynamoDB. Детально розглянемо це у розділі Best Practices.

Приклади хороших Partition Key:

  • UserId (UUID) — висока кардинальність, рівномірний розподіл
  • OrderId (UUID або timestamp-based) — унікальний для кожного запису
  • DeviceId (для IoT) — кожен пристрій має унікальний ідентифікатор

Приклади поганих Partition Key:

  • Country — лише ~200 унікальних значень, більшість запитів сконцентровано на одній-двох країнах
  • Status ("active", "inactive") — лише 2 значення, "active" буде hot partition
  • Date (тільки дата без часу) — всі записи за один день на одній партиції

Composite Primary Key: Partition Key + Sort Key

Composite Primary Key (або Range Key) складається з двох атрибутів: Partition Key та Sort Key. Ця комбінація надає DynamoDB принципово нові можливості для запитів. Partition Key визначає, на якій фізичній партиції зберігатиметься елемент, а Sort Key визначає порядок зберігання елементів всередині однієї партиції.

Ключова відмінність від простого первинного ключа: при Composite Primary Key кілька елементів можуть мати однакове значення Partition Key — це цілком допустимо і є основним патерном проектування. Унікальність елемента визначається парою (Partition Key, Sort Key).

Візуалізація Composite Primary Key

Приклад 1: Таблиця UserOrders (Замовлення користувачів)

Тут UserId є Partition Key, а OrderDate#OrderId — Sort Key. Обидва атрибути мають тип String. Унікальність запису гарантується виключно комбінацією обох значень. Зверніть увагу, що замовлення для usr-001 відсортовані хронологічно:

UserId (PK)
тип: String (S)
OrderDate#OrderId (SK)
тип: String (S)
Amount
тип: Number (N)
Status
тип: String (S)
usr-0012025-01-15T10:00:00#ord-A150.00DELIVERED
usr-0012025-03-20T14:30:00#ord-B89.99SHIPPED
usr-0012025-06-01T09:15:00#ord-C320.50PENDING
usr-0022025-02-10T08:00:00#ord-D45.00DELIVERED
usr-0022025-05-05T16:45:00#ord-E210.00DELIVERED
Приклад 2: Таблиця ChatMessages (Повідомлення в чатах)

Тут ConversationId виступає як Partition Key, групуючи всі повідомлення конкретної розмови на одній партиції. Timestamp#MessageId виступає як Sort Key, забезпечуючи хронологічний порядок та унікальність повідомлень (оскільки кілька повідомлень можуть бути відправлені в одну секунду):

ConversationId (PK)
тип: String (S)
Timestamp#MessageId (SK)
тип: String (S)
SenderId
тип: String (S)
MessageText
тип: String (S)
room-4022025-06-03T10:00:15Z#msg-001usr-001Привіт всім!
room-4022025-06-03T10:00:45Z#msg-002usr-002Привіт, Олено! Як справи?
room-4022025-06-03T10:01:10Z#msg-003usr-001Все чудово, працюю над DynamoDB.
room-5112025-06-03T11:30:00Z#msg-004usr-003Коли почнемо зустріч?
Приклад 3: Таблиця ProjectTasks (Задачі проектів з ієрархічним SK)

У великих системах Composite PK використовується для організації зв'язків типу "один-до-багатьох" та ієрархій. Розглянемо таблицю проектних задач, де Partition Key — це ProjectId, а Sort Key — це Priority#TaskId. Такий SK дозволяє не просто ідентифікувати задачу, але й сортувати задачі за пріоритетом у межах проекту:

ProjectId (PK)
тип: String (S)
Priority#TaskId (SK)
тип: String (S)
TaskTitle
тип: String (S)
AssigneeId
тип: String (S)
DueDate
тип: String (S)
proj-alpha1-CRITICAL#task-102Налаштувати AWS Credentialsusr-0012025-06-05
proj-alpha2-HIGH#task-105Створити схему DynamoDBusr-0012025-06-08
proj-alpha3-NORMAL#task-101Написати README.mdusr-0022025-06-12
proj-beta1-CRITICAL#task-201Виправити баг з авторизацієюusr-0032025-06-04

Основні особливості роботи з композитним ключем:

  • Групування та сортування: Записи з однаковим Partition Key (наприклад, usr-001) зберігаються на одній фізичній партиції та відсортовані за значенням Sort Key.
  • Ефективний діапазонний пошук: Завдяки сортуванню ви можете використовувати операцію Query для вибірки всіх замовлень користувача usr-001 за певний період (наприклад, за допомогою оператора BETWEEN для SK).
  • Складений Sort Key: Використання патерну Date#Id або Category#Status у Sort Key дозволяє будувати гнучкі та складні умови для пошуку в межах однієї партиції.

Схема нижче демонструє, як це виглядає на фізичному та логічному рівнях:

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

title "Composite Primary Key: Partition Key + Sort Key"

package "Таблиця: UserOrders" as TBL #fef3c7 {
    rectangle "Partition: UserId = \"usr-001\"" as P1 #dbeafe {
        card "SK: \"2025-01-15T10:00:00\"\nOrderId: ord-A\nAmount: 150.00" as I11 #bbf7d0
        card "SK: \"2025-03-20T14:30:00\"\nOrderId: ord-B\nAmount: 89.99" as I12 #bbf7d0
        card "SK: \"2025-06-01T09:15:00\"\nOrderId: ord-C\nAmount: 320.50" as I13 #bbf7d0

        note right of I11
          Всередині партиції елементи
          зберігаються ВІДСОРТОВАНИМИ
          за Sort Key (B-tree індекс).
          Пошук по діапазону — O(log n)
        end note
    }

    rectangle "Partition: UserId = \"usr-002\"" as P2 #dbeafe {
        card "SK: \"2025-02-10T08:00:00\"\nOrderId: ord-D\nAmount: 45.00" as I21 #bbf7d0
        card "SK: \"2025-05-05T16:45:00\"\nOrderId: ord-E\nAmount: 210.00" as I22 #bbf7d0
    }
}

note bottom of TBL
  Query: UserId = "usr-001" AND OrderDate BETWEEN "2025-01-01" AND "2025-04-01"
  → повертає: ord-A, ord-B (2 елементи з однієї партиції, без повного сканування)
end note

@enduml

Можливості запитів із Sort Key. Наявність Sort Key відкриває можливість ефективних Query-операцій із умовою на значення Sort Key. DynamoDB підтримує такі оператори для Sort Key у межах однієї партиції:

ОператорЗначенняПриклад
=Точне значенняOrderDate = "2025-06-01T10:00:00"
<, <=Менше або рівнеScore <= 100
>, >=Більше або рівнеCreatedAt >= "2025-01-01"
BETWEENВключний діапазонScore BETWEEN 50 AND 100
begins_withПочинається з префіксуOrderId begins_with "2025-06"
Важливо: оператор begins_with та всі порівняльні оператори для Sort Key працюють тільки всередині однієї партиції. Неможливо знайти всі елементи з Sort Key, що "починається з X" по всій таблиці без сканування. Для таких запитів необхідні Global Secondary Index (розглянемо у наступному розділі).

Паттерни проектування з Composite Primary Key. Розуміння цього патерну є ключовим для ефективного використання DynamoDB. Partition Key групує пов'язані елементи в одну партицію, Sort Key впорядковує їх для ефективного пошуку:

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

title "Патерни Composite Primary Key у реальних доменах"

package "Приклад 1: Замовлення користувача" as EX1 #fef3c7 {
    note as N1
      PK: UserId | SK: OrderDate#OrderId
      ─────────────────────────────────────
      usr-001 | 2025-01-15#ord-A → Order A
      usr-001 | 2025-03-20#ord-B → Order B
      usr-001 | 2025-06-01#ord-C → Order C
      usr-002 | 2025-02-10#ord-D → Order D
      ─────────────────────────────────────
      Query: "всі замовлення usr-001 за Q1 2025"
      → PK="usr-001", SK BETWEEN "2025-01" AND "2025-04"
    end note
}

package "Приклад 2: Повідомлення чату" as EX2 #dbeafe {
    note as N2
      PK: ConversationId | SK: Timestamp#MessageId
      ─────────────────────────────────────────────
      conv-001 | 2025-06-01T10:00#msg-1 → "Привіт!"
      conv-001 | 2025-06-01T10:01#msg-2 → "Як справи?"
      conv-001 | 2025-06-01T10:02#msg-3 → "Чудово!"
      conv-002 | 2025-06-01T11:00#msg-4 → "Зустріч?"
      ─────────────────────────────────────────────
      Query: "останні 20 повідомлень конversation-001"
      → PK="conv-001", SK DESC, LIMIT 20
    end note
}

package "Приклад 3: IoT-дані сенсора" as EX3 #d1fae5 {
    note as N3
      PK: DeviceId | SK: Timestamp
      ─────────────────────────────────────
      sensor-A | 2025-06-01T00:00:00 → 23.5°C
      sensor-A | 2025-06-01T00:05:00 → 23.7°C
      sensor-A | 2025-06-01T00:10:00 → 24.1°C
      sensor-B | 2025-06-01T00:00:00 → 18.2°C
      ─────────────────────────────────────
      Query: "показання sensor-A за останню годину"
      → PK="sensor-A", SK >= "2025-06-01T09:00:00"
    end note
}

@enduml

Порівняльна таблиця первинних ключів

Для швидкого вибору типу первинного ключа скористайтеся порівняльною таблицею:

ХарактеристикаSimple Primary Key (Partition Key)Composite Primary Key (PK + Sort Key)
Склад ключаЛише один атрибут: Partition Key (PK)Два атрибути: Partition Key (PK) + Sort Key (SK)
УнікальністьГарантується на рівні Partition KeyГарантується комбінацією (Partition Key, Sort Key)
Зберігання данихКожен запис може мати свій унікальний PKКілька записів можуть мати однаковий PK (групуються разом)
СортуванняДані не впорядкованіДані відсортовані за значенням SK в межах одного PK
Доступні запитиЛише за точним значенням PK (GetItem)За точним PK + фільтрація по SK (Query, BETWEEN, begins_with)
Типові сценаріїТаблиці сесій, кеш, конфігурації користувачаІсторія замовлень, чат-повідомлення, логування IoT-пристроїв

Операції читання і запису: базовий API

Розуміння базових операцій DynamoDB є необхідною передумовою для обговорення продуктивності та проектування схем. DynamoDB надає чіткий поділ між операціями для одного елемента та операціями для множини елементів.

Операції з одним елементом

GetItem — найшвидша операція в DynamoDB. Отримує рівно один елемент за його повним первинним ключем (Partition Key + Sort Key, якщо є). GetItem завжди звертається до конкретної партиції — жодного сканування, жодних індексів, гарантований час O(1).

# Отримання сесії за композитним ключем
aws dynamodb get-item \
    --table-name UserSessions \
    --key '{"UserId": {"S": "usr-001"}, "SessionId": {"S": "sess-a1b2c3d4"}}' \
    --region eu-central-1

PutItem — записує або повністю замінює елемент за вказаним первинним ключем. Якщо елемент із таким ключем вже існує — він буде повністю замінений. Підтримує умовні записи (Condition Expression).

# Запис нової сесії (якщо вже існує — буде повністю перезаписано)
aws dynamodb put-item \
    --table-name UserSessions \
    --item '{
        "UserId":    {"S": "usr-001"},
        "SessionId": {"S": "sess-a1b2c3d4"},
        "CreatedAt": {"S": "2025-06-01T10:00:00Z"},
        "IsActive":  {"BOOL": true}
    }' \
    --region eu-central-1

UpdateItem — оновлює конкретні атрибути існуючого елемента, не торкаючись інших. Є атомарним: або всі зміни застосовуються, або жодна. Підтримує атомарні лічильники (Atomic Counter).

# Атомарне оновлення статусу та часу активності сесії
aws dynamodb update-item \
    --table-name UserSessions \
    --key '{"UserId": {"S": "usr-001"}, "SessionId": {"S": "sess-a1b2c3d4"}}' \
    --update-expression "SET IsActive = :false, LastActivity = :now" \
    --expression-attribute-values '{
        ":false": {"BOOL": false},
        ":now":   {"S": "2025-06-01T12:00:00Z"}
    }' \
    --region eu-central-1

DeleteItem — видаляє елемент за первинним ключем. Підтримує умовне видалення.

# Видалення сесії за її первинним ключем
aws dynamodb delete-item \
    --table-name UserSessions \
    --key '{"UserId": {"S": "usr-001"}, "SessionId": {"S": "sess-a1b2c3d4"}}' \
    --region eu-central-1

Операції з множиною елементів

Query — отримує один або кілька елементів, що відповідають заданому Partition Key та (необов'язково) умові на Sort Key. Query виконується виключно всередині однієї партиції — це забезпечує ефективність. Query — це основна операція запиту у правильно спроектованій таблиці DynamoDB.

# Пошук сесій користувача, які починаються з певного префіксу
aws dynamodb query \
    --table-name UserSessions \
    --key-condition-expression "UserId = :uid AND SessionId begins_with(:sessPrefix)" \
    --expression-attribute-values '{
        ":uid":        {"S": "usr-001"},
        ":sessPrefix": {"S": "sess-a"}
    }' \
    --region eu-central-1

Scan — проходить по всіх елементах таблиці або індексу. Scan є дорогою операцією: він читає кожен елемент, споживає одиниці пропускної здатності для кожного прочитаного елемента, навіть якщо результат фільтрується. Scan використовується виключно для адміністративних задач або однократного аналізу даних.

# Сканування всієї таблиці для пошуку активних сесій (неефективно!)
aws dynamodb scan \
    --table-name UserSessions \
    --filter-expression "IsActive = :true" \
    --expression-attribute-values '{":true": {"BOOL": true}}' \
    --region eu-central-1
Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

title "Query vs Scan: різниця у підході"

package "Таблиця: Orders (10 мільйонів елементів)" as TBL {
    together {
        node "Partition: usr-001\n5 замовлень" as P1 #bbf7d0
        node "Partition: usr-002\n3 замовлення" as P2 #bbf7d0
        node "Partition: usr-003\n8 замовлень" as P3 #bbf7d0
        node "... ще 2 млн партицій ..." as PDOTS #e5e7eb
    }
}

actor "Client" as C

rectangle "Query\nPK = 'usr-001'" as Q #d1fae5
rectangle "Scan\n(filter: Amount > 100)" as S #fee2e2

C --> Q
C --> S

Q --> P1 : "1 звернення\nдо 1 партиції\n~1ms, 5 RCU"
S --> P1 : "читає ВСЕ\n"
S --> P2 : "читає ВСЕ\n"
S --> P3 : "читає ВСЕ\n"
S --> PDOTS : "читає ВСЕ..."

note right of S #fee2e2
  Scan читає 10 млн елементів!
  Час: секунди–хвилини.
  Споживає: 10 млн RCU.
  В production: заборонено
  для operational queries.
end note

note right of Q #d1fae5
  Query читає 5 елементів
  в 1 партиції.
  Час: ~1ms.
  Споживає: 5 RCU (або 0.5 RCU
  при Eventually Consistent).
end note

@enduml
Правило DynamoDB-архітектора: якщо ваш застосунок виконує Scan для операційних запитів (тих, що відбуваються у real-time під час роботи користувача) — таблиця спроектована неправильно. Scan є прийнятним лише для одноразових адміністративних операцій (наприклад, міграція даних) або аналітики поза виробничим контуром.

Batch-операції та транзакції

BatchGetItem — дозволяє отримати до 100 елементів з однієї або кількох таблиць за один API-виклик. Внутрішньо DynamoDB паралелізує читання, тому час виконання близький до часу читання одного елемента.

# Пакетне отримання кількох сесій за один запит
aws dynamodb batch-get-item \
    --request-items '{
        "UserSessions": {
            "Keys": [
                {"UserId": {"S": "usr-001"}, "SessionId": {"S": "sess-a1b2"}},
                {"UserId": {"S": "usr-002"}, "SessionId": {"S": "sess-c3d4"}}
            ]
        }
    }' \
    --region eu-central-1

BatchWriteItem — дозволяє записати або видалити до 25 елементів за один виклик. Операції у BatchWriteItem не є атомарними — можливий частковий успіх (деякі елементи записані, деякі — ні). Елементи, що не вдалося обробити, повертаються у полі UnprocessedItems.

# Пакетний запис та видалення сесій (до 25 операцій)
aws dynamodb batch-write-item \
    --request-items '{
        "UserSessions": [
            {
                "PutRequest": {
                    "Item": {
                        "UserId": {"S": "usr-001"},
                        "SessionId": {"S": "sess-e5f6"},
                        "IsActive": {"BOOL": true}
                    }
                }
            },
            {
                "DeleteRequest": {
                    "Key": {
                        "UserId": {"S": "usr-002"},
                        "SessionId": {"S": "sess-old"}
                    }
                }
            }
        ]
    }' \
    --region eu-central-1

TransactWriteItems / TransactGetItems — атомарні транзакції через кілька елементів і таблиць (детально розглянемо у відповідному розділі).

# Атомарна транзакція: запис нової сесії та збільшення лічильника сесій користувача
aws dynamodb transact-write-items \
    --transact-items '[
        {
            "Put": {
                "TableName": "UserSessions",
                "Item": {
                    "UserId":    {"S": "usr-001"},
                    "SessionId": {"S": "sess-new"},
                    "IsActive":  {"BOOL": true}
                }
            }
        },
        {
            "Update": {
                "TableName": "Users",
                "Key": {"UserId": {"S": "usr-001"}},
                "UpdateExpression": "ADD ActiveSessionsCount :one",
                "ExpressionAttributeValues": {":one": {"N": "1"}}
            }
        }
    ]' \
    --region eu-central-1

Створення таблиці DynamoDB: консоль та AWS CLI

Консоль AWS Management Console

Найпростіший спосіб ознайомитися з DynamoDB — створити таблицю через web-консоль. Перейдіть до сервісу DynamoDBTablesCreate table.

Кроки створення таблиці UserSessions:

  1. Table name: UserSessions
  2. Partition key: UserId (тип String)
  3. Sort key: SessionId (тип String) (необов'язково, але рекомендовано)
  4. Table settings: для початку — Default settings (On-Demand capacity mode)
  5. Create table
Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

title "Таблиця UserSessions: схема з Composite Primary Key"

class "UserSessions" as T {
    **PK: UserId** (S)
    **SK: SessionId** (S)
    ──────────────────────
    CreatedAt: String (ISO8601)
    ExpiresAt: String (ISO8601)
    UserAgent: String
    IpAddress: String
    IsActive: Boolean
    DeviceInfo: Map
    Permissions: StringSet
}

note right of T
  PK + SK = Composite Primary Key
  ─────────────────────────────
  Запит: "всі активні сесії usr-001"
  → Query(PK="usr-001")
  → Filter(IsActive=true)

  Запит: "конкретна сесія"
  → GetItem(PK="usr-001", SK="sess-XYZ")

  Запит: "сесії із закінченим терміном"
  → потребує GSI (розглянемо далі)
end note

@enduml

Створення через AWS CLI та PowerShell

AWS CLI надає повний контроль над параметрами таблиці та є основою для автоматизації та Infrastructure-as-Code:

# Створення таблиці UserSessions з Composite Primary Key
aws dynamodb create-table \
    --table-name UserSessions \
    --attribute-definitions \
        AttributeName=UserId,AttributeType=S \
        AttributeName=SessionId,AttributeType=S \
    --key-schema \
        AttributeName=UserId,KeyType=HASH \
        AttributeName=SessionId,KeyType=RANGE \
    --billing-mode PAY_PER_REQUEST \
    --region eu-central-1
Створення таблиці UserSessions
$ aws dynamodb create-table --table-name UserSessions ...
{
"TableDescription": {
"TableName": "UserSessions",
"TableStatus": "CREATING",
"TableArn": "arn:aws:dynamodb:eu-central-1:123456789012:table/UserSessions",
"KeySchema": [
{ "AttributeName": "UserId", "KeyType": "HASH" },
{ "AttributeName": "SessionId", "KeyType": "RANGE" }
],
"BillingModeSummary": {
"BillingMode": "PAY_PER_REQUEST"
}
}
}
# Дочекатися поки таблиця перейде у стан ACTIVE
aws dynamodb wait table-exists \
    --table-name UserSessions \
    --region eu-central-1

echo "Таблиця UserSessions готова"

# Перевірити статус таблиці
aws dynamodb describe-table \
    --table-name UserSessions \
    --region eu-central-1 \
    --query "Table.{Status: TableStatus, ItemCount: ItemCount, SizeBytes: TableSizeBytes}"
Статус таблиці після створення
$ aws dynamodb describe-table --table-name UserSessions ...
{
"Status": "ACTIVE",
"ItemCount": 0,
"SizeBytes": 0
}

CRUD-операції

# ── PutItem: записати новий елемент ───────────────────────────────────────
aws dynamodb put-item \
    --table-name UserSessions \
    --item '{
        "UserId":    {"S": "usr-001"},
        "SessionId": {"S": "sess-a1b2c3d4"},
        "CreatedAt": {"S": "2025-06-01T10:00:00Z"},
        "ExpiresAt": {"S": "2025-06-01T22:00:00Z"},
        "IpAddress": {"S": "203.0.113.42"},
        "UserAgent": {"S": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"},
        "IsActive":  {"BOOL": true}
    }' \
    --region eu-central-1

# ── GetItem: отримати один елемент ────────────────────────────────────────
aws dynamodb get-item \
    --table-name UserSessions \
    --key '{"UserId": {"S": "usr-001"}, "SessionId": {"S": "sess-a1b2c3d4"}}' \
    --region eu-central-1

# ── Query: знайти всі сесії користувача ──────────────────────────────────
aws dynamodb query \
    --table-name UserSessions \
    --key-condition-expression "UserId = :uid" \
    --expression-attribute-values '{":uid": {"S": "usr-001"}}' \
    --region eu-central-1

# ── UpdateItem: деактивувати сесію ────────────────────────────────────────
aws dynamodb update-item \
    --table-name UserSessions \
    --key '{"UserId": {"S": "usr-001"}, "SessionId": {"S": "sess-a1b2c3d4"}}' \
    --update-expression "SET IsActive = :false" \
    --expression-attribute-values '{":false": {"BOOL": false}}' \
    --region eu-central-1

# ── DeleteItem: видалити сесію ───────────────────────────────────────────
aws dynamodb delete-item \
    --table-name UserSessions \
    --key '{"UserId": {"S": "usr-001"}, "SessionId": {"S": "sess-a1b2c3d4"}}' \
    --region eu-central-1
Query: всі сесії користувача usr-001
$ aws dynamodb query --table-name UserSessions --key-condition-expression "UserId = :uid" ...
{
"Items": [
{
"UserId": { "S": "usr-001" },
"SessionId": { "S": "sess-a1b2c3d4" },
"IsActive": { "BOOL": true },
"CreatedAt": { "S": "2025-06-01T10:00:00Z" }
}
],
"Count": 1,
"ScannedCount": 1
}

Одиниці пропускної здатності: RCU та WCU

Для розробки ефективних архітектур на базі Amazon DynamoDB та прогнозування витрат критично важливо розуміти її внутрішню модель тарифікації та розподілу ресурсів. На відміну від традиційних реляційних СУБД, де продуктивність лімітується апаратними характеристиками сервера (кількістю ядер CPU, обсягом RAM та IOPS накопичувачів), DynamoDB абстрагує фізичні ресурси за допомогою одиниць пропускної здатності (Capacity Units).

Цей підхід дозволяє гарантувати передбачувану пропускну здатність незалежно від загального обсягу даних у таблиці. Вся ємність вимірюється двома метриками: Read Capacity Units (RCU) для операцій читання та Write Capacity Units (WCU) для операцій запису.


Read Capacity Units (RCU)

1 RCU (Read Capacity Unit) визначається як потужність системи, необхідна для виконання:

  • 1 Strongly Consistent Read (сильно узгодженого читання) одного елемента розміром до 4 KB за секунду.
  • 2 Eventually Consistent Reads (кінцево узгоджених читань) елементів розміром до 4 KB за секунду.
  • 0.5 Transactional Read (транзакційного читання) одного елемента розміром до 4 KB за секунду (тобто для транзакційного читання 1 елемента до 4 KB потрібно 2 RCU).

Моделі узгодженості читання (Read Consistency Models)

  1. Eventually Consistent Reads (Кінцева узгодженість): При записі даних DynamoDB асинхронно реплікує зміни на три географічно розподілені вузли зберігання (storage nodes) в межах однієї Availability Zone. Eventually Consistent читання повертає результат з будь-якої випадкової репліки. Існує мінімальна ймовірність (зазвичай < 1 секунди), що запит поверне застарілі дані, якщо реплікація ще не завершилась. Цей режим споживає вдвічі менше ресурсів — 0.5 RCU за кожні 4 KB.
  2. Strongly Consistent Reads (Сильна узгодженість): Запит направляється до реплік та очікує підтвердження від більшості вузлів (quorum), гарантуючи повернення найактуальнішого стану даних. Цей режим є дорожчим та споживає 1 RCU за кожні 4 KB.
  3. Transactional Reads (Транзакційне читання): Використовується в межах ACID-транзакцій через API TransactGetItems. Забезпечує ізольовано та атомарно читання групи елементів. Споживає 2 RCU за кожні 4 KB.

Математичне округлення розміру елементів

При обчисленні RCU розмір кожного зчитуваного елемента спочатку округляється в більшу сторону до найближчого кратного 4 KB. Наприклад:

  • Елемент розміром 2.5 KB округляється до 4 KB (1 блок).
  • Елемент розміром 5.0 KB округляється до 8 KB (2 блоки).

Формула розрахунку RCU

Математична модель розрахунку необхідної кількості RCU виглядає наступним чином:

Завантаження...
\text{RCU} = \left\lceil \frac{\text{Розмір елемента (KB)}}{4\text{ KB}} \right\rceil \times \text{Кількість операцій/сек} \times \text{Коефіцієнт узгодженості}

Де Коефіцієнт узгодженості дорівнює:

  • 0.5 — для Eventually Consistent читання.
  • 1.0 — для Strongly Consistent читання.
  • 2.0 — для Transactional читання (TransactGetItems).

Покрокові приклади розрахунку RCU

  • Приклад 1: Масове читання профілів користувачів
    • Умова: Потрібно забезпечити 150 Eventually Consistent читань на секунду. Розмір одного профілю (елемента) становить 3.5 KB.
    • Крок 1 (Округлення розміру): ⌈3.5 KB / 4 KB⌉ = 1 блок.
    • Крок 2 (Розрахунок за формулою): 1 × 150 × 0.5 = 75 RCU.
  • Приклад 2: Читання фінансових транзакцій з високою узгодженістю
    • Умова: Необхідно виконувати 80 Strongly Consistent читань на секунду. Розмір елемента транзакції — 9.2 KB.
    • Крок 1 (Округлення розміру): ⌈9.2 KB / 4 KB⌉ = ⌈2.3 блоки⌉ = 3 блоки.
    • Крок 2 (Розрахунок за формулою): 3 × 80 × 1.0 = 240 RCU.
  • Приклад 3: Транзакційна перевірка балансу
    • Умова: Потрібно зчитувати 50 балансів на секунду через TransactGetItems. Розмір запису балансу — 1.5 KB.
    • Крок 1 (Округлення розміру): ⌈1.5 KB / 4 KB⌉ = 1 блок.
    • Крок 2 (Розрахунок за формулою): 1 × 50 × 2.0 = 100 RCU.

Write Capacity Units (WCU)

1 WCU (Write Capacity Unit) визначається як потужність системи, необхідна для виконання:

  • 1 стандартного запису (PutItem, UpdateItem, DeleteItem) одного елемента розміром до 1 KB за секунду.
  • 0.5 транзакційного запису (TransactWriteItems) одного елемента розміром до 1 KB за секунду (тобто для транзакційного запису 1 елемента до 1 KB потрібно 2 WCU).

Особливості операцій запису

  1. Співвідношення з читанням: Записи в DynamoDB є в 4 рази "дорожчими" з точки зору розміру даних: якщо 1 RCU покриває 4 KB, то 1 WCU покриває лише 1 KB. Це зумовлено необхідністю синхронної реплікації змін на кілька фізичних вузлів для запобігання втрати даних.
  2. Умовні записи (Conditional Writes): Використання ConditionExpression (наприклад, перевірка чи існує email перед створенням користувача) не змінює вартість успішного запису. Однак, якщо умова не виконується і запис скасовується, DynamoDB все одно стягує WCU за перевірку, якщо запит намагався перезаписати існуючий елемент.
  3. Транзакційні записи (Transactional Writes): Виконуються через TransactWriteItems для забезпечення атомарності групи записів (до 100 елементів). Кожен запис у транзакції коштує вдвічі дорожче — 2 WCU за 1 KB.

Математичне округшення розміру елементів

При обчисленні WCU розмір кожного записуваного або оновлюваного елемента округляється в більшу сторону до найближчого кратного 1 KB. Наприклад:

  • Запис розміром 450 байт округляється до 1 KB (1 блок).
  • Запис розміром 2.1 KB округляється до 3 KB (3 блоки).

Формула розрахунку WCU

Математична модель розрахунку необхідної кількості WCU:

Завантаження...
\text{WCU} = \left\lceil \frac{\text{Розмір елемента (KB)}}{1\text{ KB}} \right\rceil \times \text{Кількість операцій/сек} \times \text{Коефіцієнт запису}

Де Коефіцієнт запису дорівнює:

  • 1.0 — для стандартних записів (Put, Update, Delete, включно з умовними).
  • 2.0 — для транзакційних записів (TransactWriteItems).

Покрокові приклади розрахунку WCU

  • Приклад 1: Логування IoT-метрик
    • Умова: Пристрій відправляє 200 метрик на секунду. Розмір одного повідомлення — 600 байт.
    • Крок 1 (Округлення розміру): ⌈0.6 KB / 1 KB⌉ = 1 блок.
    • Крок 2 (Розрахунок за формулою): 1 × 200 × 1.0 = 200 WCU.
  • Приклад 2: Оновлення профілів користувачів
    • Умова: Користувачі роблять 45 оновлень профілю на секунду. Розмір оновленого запису — 2.4 KB.
    • Крок 1 (Округлення розміру): ⌈2.4 KB / 1 KB⌉ = 3 блоки.
    • Крок 2 (Розрахунок за формулою): 3 × 45 × 1.0 = 135 WCU.
  • Приклад 3: Оформлення замовлення транзакцією
    • Умова: Система обробляє 10 замовлень на секунду. Транзакція записує 1 замовлення (розмір 1.2 KB) та оновлює складські запаси (розмір 0.8 KB).
    • Крок 1 (Замовлення): Розмір 1.2 KB оновлюється до 2 KB. Вартість: 2 × 10 × 2.0 = 40 WCU.
    • Крок 2 (Склад): Розмір 0.8 KB оновлюється до 1 KB. Вартість: 1 × 10 × 2.0 = 20 WCU.
    • Загальна вартість: 40 + 20 = 60 WCU.

Зведена порівняльна таблиця вартості ємностей

ОпераціяОдиниці базового виміруКоефіцієнт (Multiplier)Вартість 1 операції до базового розміру
Eventually Consistent Read4 KB0.50.5 RCU
Strongly Consistent Read4 KB1.01.0 RCU
Transactional Read4 KB2.02.0 RCU
Standard Write (Put/Update/Delete)1 KB1.01.0 WCU
Transactional Write1 KB2.02.0 WCU

::

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

title "Розрахунок потрібних RCU/WCU для UserSessions API"

rectangle "Вимоги системи (peak load)" as REQ #fef3c7 {
    note as REQ_NOTE
      - 500 GetItem (сесій/с)   — розмір ~0.5 KB, eventually consistent
      - 200 PutItem (нових сесій/с) — розмір ~0.8 KB
      - 100 Query (списків сесій/с) — по 5 елементів, ~0.5 KB кожен
      - 50  UpdateItem (оновлень/с) — розмір ~0.8 KB
    end note
}

rectangle "Розрахунок RCU" as CALC_RCU #dbeafe {
    note as CALC_RCU_NOTE
      GetItem: ceil(0.5/4) × 500 × 0.5 = 1 × 500 × 0.5 = 250 RCU
      Query:   ceil(0.5/4) × 5 elem × 100 × 0.5 = 250 RCU
      ─────────────────────────────────────────────────
      Разом RCU: 250 + 250 = 500 RCU
    end note
}

rectangle "Розрахунок WCU" as CALC_WCU #d1fae5 {
    note as CALC_WCU_NOTE
      PutItem:    ceil(0.8/1) × 200 = 1 × 200 = 200 WCU
      UpdateItem: ceil(0.8/1) × 50  = 1 × 50  =  50 WCU
      ─────────────────────────────────────────────────
      Разом WCU: 200 + 50 = 250 WCU
    end note
}

REQ --> CALC_RCU
REQ --> CALC_WCU

note bottom of CALC_WCU
  У режимі On-Demand: платите лише за
  використані RCU/WCU — не потрібно
  прогнозувати заздалегідь.
  У режимі Provisioned: виставити
  Provisioned RCU=500, WCU=250
  (або з Auto Scaling).
end note

@enduml

Secondary Indexes: запити поза первинним ключем

Ми з'ясували фундаментальне обмеження DynamoDB: операція Query може шукати елементи лише за Partition Key основної таблиці. Це означає, що таблиця UserSessions з PK=UserId / SK=SessionId ефективно відповідає на запитання «які сесії має користувач X?», але абсолютно не здатна відповісти на «які сесії закінчились до певної дати?» або «які сесії з конкретної IP-адреси?» — без повного Scan всієї таблиці.

DynamoDB вирішує цю проблему через Secondary Indexes — механізм, що дозволяє визначити альтернативну схему ключів для таблиці та виконувати Query за цією альтернативною схемою. Існує два типи Secondary Indexes з принципово різними характеристиками.

Для наочності розглянемо поточний стан основної таблиці UserSessions (з первинним ключем UserId + SessionId), на прикладі якої ми будемо вивчати обидва типи індексів:

Вихідний набір даних таблиці UserSessions:

UserId (Partition Key)SessionId (Sort Key)ExpiresAtIpAddressIsActiveCreatedAt
usr-001sess-A2025-06-01T22:00:00Z203.0.113.42true2025-06-01T10:00:00Z
usr-001sess-B2025-06-02T10:00:00Z198.51.100.7true2025-06-01T11:00:00Z
usr-001sess-C2025-05-31T08:00:00Z203.0.113.42false2025-05-30T16:00:00Z
usr-002sess-D2025-06-03T14:00:00Z10.0.0.5true2025-06-01T13:00:00Z

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

title "Secondary Indexes: два типи з різними обмеженнями"

package "Таблиця: UserSessions" as MAIN #fef3c7 {
    note as MAIN_NOTE
      PK: UserId (S)
      SK: SessionId (S)
      ──────────────────
      ExpiresAt: String
      IpAddress: String
      IsActive: Boolean
      CreatedAt: String
    end note
}

package "LSI: SessionsByExpiry" as LSI #dbeafe {
    note as LSI_NOTE
      PK: UserId (S) ← той самий!
      SK: ExpiresAt (S) ← новий!
      ──────────────────
      ProjectionType: ALL
      ──────────────────
      Обмеження:
      - Той самий PK що й таблиця
      - Тільки при создані таблиці
      - Max 5 LSI на таблицю
      - 10 GB ліміт на партицію
    end note
}

package "GSI: SessionsByIp" as GSI #d1fae5 {
    note as GSI_NOTE
      PK: IpAddress (S) ← будь-який атрибут!
      SK: CreatedAt (S) ← будь-який атрибут!
      ──────────────────
      ProjectionType: KEYS_ONLY
      ──────────────────
      Обмеження:
      - Окрема пропускна здатність
      - Eventual consistency лише
      - Додається в будь-який момент
      - Max 20 GSI на таблицю
    end note
}

MAIN -right-> LSI : "автоматично синхронізується"
MAIN -down-> GSI  : "автоматично синхронізується"

@enduml

Local Secondary Index (LSI)

Local Secondary Index (LSI) представляє собою альтернативний індекс доступу, який створюється в межах однієї логічної партиції. Ключовою архітектурною особливістю LSI є те, що він зобов'язаний використовувати той самий Partition Key (HASH), що й основна таблиця, але дозволяє визначити інший атрибут як Sort Key (RANGE).

Термін «локальний» вказує на фізичну локалізацію даних: всі проектовані елементи індексу, що відповідають певному Partition Key, зберігаються на тому самому фізичному вузлі (storage node) і в межах тієї ж групи партицій, що й відповідні оригінальні елементи основної таблиці. Це забезпечує сувору гарантію транзакційної узгодженості та мінімальних затримок при зверненні.

Архітектурна доцільність та патерни застосування LSI

Необхідність використання LSI виникає у сценаріях, коли для однієї сутності (ідентифікованої за Partition Key) потрібно виконувати складні запити з різними критеріями сортування або фільтрації.

Розглянемо практичний сценарій проектування таблиці сесій користувачів UserSessions, де первинний ключ побудований як composite: Partition Key = UserId, Sort Key = SessionId. Ця схема дозволяє виконувати ефективні операції точкового читання (GetItem) конкретної сесії або отримувати перелік усіх сесій користувача (Query).

Проте, якщо виникає патерн доступу виду «отримати всі сесії конкретного користувача, термін дії яких закінчується до заданого моменту» (наприклад, для інвалідації сесій або виведення попереджень користувачу), стандартна схема вимагатиме виконання запиту Query за UserId з подальшою фільтрацією за полем ExpiresAt на стороні додатка або через FilterExpression. Це призведе до зайвого споживання RCU, оскільки DynamoDB зчитує всі сесії користувача, перш ніж застосувати фільтр.

Створення LSI з назвою SessionsByExpiry, де Sort Key визначено як ExpiresAt, дозволяє виконувати прямі сортувальні запити (Query) в межах одного UserId з накладанням умов на ExpiresAt безпосередньо на рівні бази даних.

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

title "LSI SessionsByExpiry: Query за ExpiresAt в межах UserId"

package "Основна таблиця: UserSessions" as MAIN #fef3c7 {
    card "PK:usr-001 | SK:sess-A\nExpiresAt: 2025-06-01T22:00\nIsActive: true" as I1 #bbf7d0
    card "PK:usr-001 | SK:sess-B\nExpiresAt: 2025-06-02T10:00\nIsActive: true" as I2 #bbf7d0
    card "PK:usr-001 | SK:sess-C\nExpiresAt: 2025-05-31T08:00\nIsActive: false" as I3 #fde68a
    card "PK:usr-002 | SK:sess-D\nExpiresAt: 2025-06-03T14:00\nIsActive: true" as I4 #bbf7d0
}

package "LSI: SessionsByExpiry" as LSI #dbeafe {
    note as LSI_NOTE
      PK: UserId (той самий!)
      SK: ExpiresAt (новий!)
      ─────────────────────────────
      usr-001 | 2025-05-31T08:00 → sess-C
      usr-001 | 2025-06-01T22:00 → sess-A
      usr-001 | 2025-06-02T10:00 → sess-B
      usr-002 | 2025-06-03T14:00 → sess-D
      ─────────────────────────────
      Відсортовано по ExpiresAt
      всередині кожного UserId!
    end note
}

MAIN -right-> LSI : "автоматична реплікація"

note bottom of LSI
  Query: UserId="usr-001" AND ExpiresAt < "2025-06-02T00:00"
  → повертає: sess-C (вже протермінована), sess-A (закінчується сьогодні)
  → БЕЗ сканування sess-B, sess-D
end note

@enduml

Системні обмеження та архітектурні компроміси LSI

Хоча LSI надає перевагу у вигляді сильної узгодженості та атомарності, його використання супроводжується жорсткими обмеженнями, які необхідно враховувати на етапі проектування.

1. Обмеження розміру колекції елементів (10 GB Item Collection Limit)

Найбільш критичним обмеженням LSI є ліміт на розмір колекції елементів (Item Collection). Колекція елементів — це сукупність усіх елементів у таблиці та в усіх її локальних вторинних індексах, які мають однакове значення Partition Key.

Фізично, для забезпечення низьких затримок, DynamoDB зберігає всю колекцію елементів на одному фізичному вузлі партиції. Через це сумарний розмір колекції елементів для одного значення Partition Key не може перевищувати 10 GB. Якщо обсяг даних одного ключа (наприклад, сесій одного активного користувача) перевищить цей ліміт, будь-які подальші операції запису або оновлення для цього Partition Key завершаться помилкою ValidationException.

Практичний висновок: LSI абсолютно не підходить для сутностей з необмеженим зростанням даних (наприклад, логування подій IoT-пристроїв за ідентифікатором пристрою, де обсяг логів на один пристрій гарантовано перевищить 10 GB).

2. Неможливість динамічного керування

Локальний вторинний індекс може бути створений виключно в момент ініціалізації таблиці (CreateTable). Додати LSI до існуючої таблиці або видалити його пізніше неможливо. Якщо в процесі експлуатації виявиться потреба в новому LSI, єдиним виходом буде створення нової таблиці, налаштування нової схеми індексів та міграція всіх історичних даних, що є тривалим та дорогим процесом.


Стратегії проектування індексів: Типи проекцій (Projection Types)

При створенні вторинного індексу розробник повинен визначити, які саме атрибути з основної таблиці будуть копіюватися (проектувалися) в індекс. Це визначається параметром Projection. Вибір типу проекції є компромісом між обсягом збережених даних (витрати на Storage та WCU) та продуктивністю запитів (витрати на RCU).

KEYS_ONLY

Індекс зберігає лише ключові атрибути: PK та SK основної таблиці + PK та SK індексу. Мінімальний розмір → мінімальні витрати. Якщо потрібні інші атрибути — DynamoDB автоматично виконує додаткове читання основної таблиці.

Коли: часто використовується як «існування» (перевірити чи є такий запис), або якщо майже завжди потрібно також читати основну таблицю для деталей.

INCLUDE

Зберігає ключові атрибути + явно вказаний список атрибутів (NonKeyAttributes). Дозволяє точно контролювати, які поля дублюються в індексі.

Коли: коли відомий фіксований набір атрибутів, що потрібні при Query через індекс. Наприклад, ExpiresAt + IsActive — достатньо для перевірки статусу сесії без читання повного елемента.

ALL

Зберігає всі атрибути елемента. Максимальний розмір індексу — фактично дублювання всієї таблиці. Але Query через індекс повертає повний елемент без додаткових читань.

Коли: якщо при Query через індекс завжди потрібні всі атрибути, і витрати на зберігання прийнятні.

Практичне правило для Projection: починайте з KEYS_ONLY або INCLUDE (мінімально необхідний набір атрибутів для вашого Query). ALL зручний, але подвоює або потроює витрати на зберігання. DynamoDB не дозволяє змінити Projection після створення індексу — треба перестворювати.

Вплив LSI на споживання RCU та WCU

Використання локальних індексів суттєво оновлює можливості запитів, проте створює додаткове навантаження на пропускну здатність таблиці:

  1. Витрати на запис (Write Cost Dynamics): При додаванні (PutItem), оновленні (UpdateItem) або видаленні (DeleteItem) елемента в основній таблиці DynamoDB автоматично оновлює дані у всіх LSI.
    • Якщо записується новий елемент, споживається 1 WCU (або 2 WCU для транзакцій) за кожен 1 KB розміру елемента як для основної таблиці, так і для кожного LSI, куди проектується цей елемент.
    • Якщо оновлюється атрибут елемента, який не входить у проекцію LSI, додаткові WCU для цього індексу не споживаються.
    • Якщо оновлюється атрибут, що входить до проекції індексу (або змінюється сам ключ індексу Sort Key), DynamoDB виконує дві операції в індексі (видалення старого запису та запис нового), що споживає додаткові WCU.

    Усі витрати WCU на обслуговування LSI списуються з пулу пропускної здатності основної таблиці.
  2. Витрати на читання та явище Main Table Fetch (Read Cost Dynamics): При виконанні запиту Query через LSI вартість у RCU залежить від вибраного типу проекції:
    • Покритий запит (Covered Query): Якщо запит запитує лише ті атрибути, які були спроектовані в індекс (KEYS_ONLY або INCLUDE), DynamoDB зчитує дані виключно з LSI. Розрахунок RCU відбувається стандартно: 1 RCU на 4 KB спроектованих даних (Strongly Consistent).
    • Непокритий запит та Main Table Fetch: Якщо запит звертається до атрибутів, які відсутні в проекції індексу (наприклад, у проекції KEYS_ONLY запитується поле IpAddress), DynamoDB змушена виконати внутрішню операцію вибірки з основної таблиці (Main Table Fetch / Lookup) для кожного знайденого елемента.

    Штраф за Fetch: Кожна така вибірка з основної таблиці виконується як окреме Strongly Consistent читання і споживає щонайменше 1 RCU на кожен елемент (навіть якщо розмір елемента менший за 4 KB) та додає затримку (latency) на мережеве коло всередині бази даних.
    Формула розрахунку RCU для непокритого запиту:
    Завантаження...
    \text{RCU}{\text{Total}} = \text{RCU}{\text{Index Query}} + \text{Кількість знайдених елементів} \times 1\text{ RCU}

Створення таблиці з LSI

# Таблиця UserSessions з LSI SessionsByExpiry
aws dynamodb create-table \
    --table-name UserSessions \
    --attribute-definitions \
        AttributeName=UserId,AttributeType=S \
        AttributeName=SessionId,AttributeType=S \
        AttributeName=ExpiresAt,AttributeType=S \
    --key-schema \
        AttributeName=UserId,KeyType=HASH \
        AttributeName=SessionId,KeyType=RANGE \
    --local-secondary-indexes '[
        {
            "IndexName": "SessionsByExpiry",
            "KeySchema": [
                {"AttributeName": "UserId",    "KeyType": "HASH"},
                {"AttributeName": "ExpiresAt", "KeyType": "RANGE"}
            ],
            "Projection": {
                "ProjectionType": "INCLUDE",
                "NonKeyAttributes": ["IsActive", "IpAddress"]
            }
        }
    ]' \
    --billing-mode PAY_PER_REQUEST \
    --region eu-central-1

Query через LSI

# Знайти сесії usr-001, що закінчаться до 2025-06-02T00:00:00Z
aws dynamodb query \
    --table-name UserSessions \
    --index-name SessionsByExpiry \
    --key-condition-expression "UserId = :uid AND ExpiresAt < :exp" \
    --expression-attribute-values '{
        ":uid": {"S": "usr-001"},
        ":exp": {"S": "2025-06-02T00:00:00Z"}
    }' \
    --region eu-central-1
Query через LSI SessionsByExpiry
$ aws dynamodb query --index-name SessionsByExpiry --key-condition-expression "UserId = :uid AND ExpiresAt < :exp" ...
{
"Items": [
{
"UserId": { "S": "usr-001" },
"SessionId": { "S": "sess-C" },
"ExpiresAt": { "S": "2025-05-31T08:00:00Z" },
"IsActive": { "BOOL": false },
"IpAddress": { "S": "203.0.113.42" }
},
{
"UserId": { "S": "usr-001" },
"SessionId": { "S": "sess-A" },
"ExpiresAt": { "S": "2025-06-01T22:00:00Z" },
"IsActive": { "BOOL": true },
"IpAddress": { "S": "198.51.100.7" }
}
],
"Count": 2,
"ScannedCount": 2
}

Global Secondary Index (GSI)

Global Secondary Index (GSI) представляє собою найбільш гнучкий та потужний інструмент побудови вторинних ключів доступу в Amazon DynamoDB. На відміну від LSI, GSI дозволяє обрати абсолютно довільні атрибути таблиці як в якості Partition Key (HASH), так і Sort Key (RANGE), повністю ігноруючи схему первинного ключа основної таблиці. Слово «Global» у назві вказує на те, що запити через цей індекс можуть охоплювати дані на всіх фізичних партиціях основної таблиці.

Архітектурні особливості та фізична реалізація GSI

Під капом Amazon DynamoDB реалізує GSI як повністю автономну внутрішню таблицю, яка автоматично синхронізується з основною таблицею. Ця внутрішня таблиця має власну схему розділення на фізичні партиції, власні вузли зберігання (storage nodes) і, що найважливіше, — свій власний незалежний пул пропускної здатності (RCU та WCU) у випадку використання режиму Provisioned.

Така фізична автономність зумовлює три фундаментальні архітектурні наслідки:

  1. Гарантована кінцева узгодженість (Eventually Consistent Reads): Синхронізація даних між основною таблицею та GSI виконується асинхронно за допомогою внутрішнього механізму реплікації. Через це запити (Query) через GSI підтримують виключно Eventually Consistent reads. Зчитування з Strongly Consistent або в межах транзакцій безпосередньо через GSI є неможливим, оскільки реплікація в індекс відбувається з мінімальною затримкою (зазвичай до кількох десятків мілісекунд, але за умови перевантаження індексу затримка може зростати).
  2. Автономне керування ємністю (Independent Throughput): У режимі Provisioned для GSI необхідно окремо налаштовувати ліміти RCU та WCU. Вони ніяк не пов'язані з лімітами основної таблиці та оплачуються окремо.
  3. Ефект "зворотного тиску" при перевантаженні індексу (GSI Throttling Backpressure): Оскільки реплікація в GSI відбувається асинхронно, при високій інтенсивності записів на основну таблицю індекс може не встигати оновлюватися, якщо його ліміт WCU занижений. Щоб запобігти безконтрольному відставанню реплікації та переповненню черг оновлень, DynamoDB застосовує зворотний тиск: помилки недостатньої ємності (throttling) на GSI будуть каскадно блокувати операції запису в основній таблиці.
    Критичне правило: навіть якщо основна таблиця має надлишок WCU, запис до неї завершиться помилкою ProvisionedThroughputExceededException, якщо асоційований GSI вичерпає власні WCU.
Loading diagram...
@startuml
!theme plain
skinparam backgroundColor #FAFAFA
skinparam defaultFontName "DejaVu Sans"

title GSI SessionsByIp: Запити за IpAddress без прив'язки до UserId

package "Основна таблиця: UserSessions" as MAIN #E8F4FD {
    note as MAIN_DATA
      PK:usr-001 | SK:sess-A | IP:203.0.113.42 | CreatedAt:2025-06-01T10:00
      PK:usr-001 | SK:sess-B | IP:198.51.100.7 | CreatedAt:2025-06-01T11:00
      PK:usr-002 | SK:sess-C | IP:203.0.113.42 | CreatedAt:2025-06-01T12:00
      PK:usr-003 | SK:sess-D | IP:10.0.0.5     | CreatedAt:2025-06-01T13:00
    end note
}

package "GSI індекс: SessionsByIp" as GSI #D5E8D4 {
    note as GSI_DATA
      GSI-PK: IpAddress (HASH) | GSI-SK: CreatedAt (RANGE)
      ────────────────────────────────────────────────────
      10.0.0.5     | 2025-06-01T13:00 → usr-003/sess-D
      198.51.100.7 | 2025-06-01T11:00 → usr-001/sess-B
      203.0.113.42 | 2025-06-01T10:00 → usr-001/sess-A
      203.0.113.42 | 2025-06-01T12:00 → usr-002/sess-C
      ────────────────────────────────────────────────────
      Сортування: HASH (IpAddress) -> RANGE (CreatedAt)
    end note
}

MAIN -right-> GSI : "Асинхронна реплікація\n(Eventual Consistency)"

note bottom of GSI
  Запит (Query): IpAddress = "203.0.113.42"
  * Повертає: usr-001/sess-A + usr-002/sess-C
  * Поєднує дані різних користувачів!
  * Не вимагає дорогого Scan основної таблиці
end note
@enduml

Sparse Index (Розріджені індекси) — паттерн оптимізації обсягу та витрат

Sparse Index (розріджений індекс) — це фундаментальний патерн проектування вторинних індексів у DynamoDB, який використовує вибіркову поведінку оновлення індексів.

За замовчуванням вторинний індекс у DynamoDB вважається розрідженим, якщо будь-який елемент основної таблиці не містить атрибутів, визначених як Partition Key або Sort Key цього індексу. DynamoDB не створює запис в індексі, якщо у вихідному елементі відсутній хоча б один із ключів індексу.

Переваги та архітектурна цінність розріджених індексів:

  1. Скорочення витрат на зберігання (Storage Savings): Замість копіювання мільярдів записів, індекс містить лише ту вибіркову підмножину даних, яка відповідає певній умові бізнес-логіки.
  2. Мінімізація WCU (Write Savings): DynamoDB оновлює GSI лише тоді, коли створюється або змінюється елемент, що містить ключові атрибути індексу. Зміна будь-яких інших елементів таблиці не витрачає пропускну здатність GSI.
  3. Висока швидкість виконання запитів (Query Efficiency): Оскільки індекс компактний, операції Query виконуються за мінімальну кількість звернень, без потреби сканувати зайві дані.

Сценарій застосування: Обробка незавершених замовлень (Pending Orders)

Уявімо велику e-commerce систему з мільярдами замовлень, де 99% замовлень знаходяться у кінцевих статусах (DELIVERED, CANCELLED), і лише 1% замовлень перебуває в процесі обробки (PENDING).

Операторам потрібно постійно отримувати список замовлень для обробки. Сканування всієї таблиці є неприпустимо дорогим. Якщо ми створимо GSI з ключем сортування, який заповнюється значенням дати виключно для замовлень зі статусом PENDING (наприклад, атрибут PendingStatus заповнюється лише тоді, коли статус дорівнює PENDING), індекс міститиме лише цей 1% активних замовлень. Як тільки замовлення доставляється, атрибут PendingStatus видаляється з елемента, і DynamoDB автоматично прибирає цей запис із розрідженого GSI.

Loading diagram...
@startuml
!theme plain
skinparam backgroundColor #FAFAFA
skinparam defaultFontName "DejaVu Sans"

title Sparse GSI: Індексуються лише елементи з атрибутом NeedsProcessing

package "Основна таблиця: Orders (1 млн елементів)" as MAIN #E8F4FD {
    card "ord-001\nStatus: DELIVERED\n(NeedsProcessing відсутній)" as O1 #D5E8D4
    card "ord-002\nStatus: PENDING\nNeedsProcessing: 'true'" as O2 #FFF2CC
    card "ord-003\nStatus: DELIVERED\n(NeedsProcessing відсутній)" as O3 #D5E8D4
    card "ord-004\nStatus: PENDING\nNeedsProcessing: 'true'" as O4 #FFF2CC
}

package "Sparse GSI: PendingOrders\nGSI-PK: NeedsProcessing" as GSI #F0E6FF {
    note as GSI_NOTE
      Індекс містить лише 2 елементи!
      ───────────────────────────
      NeedsProcessing='true' → ord-002
      NeedsProcessing='true' → ord-004
      ───────────────────────────
      Елементи ord-001 та ord-003
      повністю ігноруються індексом
    end note
}

O2 -right-> GSI : "Проектується (атрибут наявний)"
O4 -right-> GSI : "Проектується (атрибут наявний)"
O1 ..> GSI : "Не проектується"
O3 ..> GSI : "Не проектується"

note bottom of GSI
  Запит (Query): NeedsProcessing = 'true'
  * Отримує 2 елементи замість сканування 1 млн
  * При оновленні статусу видаляємо NeedsProcessing
  * Елемент автоматично видаляється з GSI
end note
@enduml

Додавання GSI до існуючої таблиці та фонове заповнення (Backfilling)

На відміну від локальних індексів (LSI), глобальні індекси (GSI) є динамічними сутностями. Їх можна створювати, оновлювати або видаляти на будь-му етапі життєвого циклу таблиці, навіть якщо вона містить мільярди записів.

Процес онлайн-індексації та фонового заповнення (Backfilling)

Створення GSI на існуючій таблиці є складною розподіленою операцією, яка виконується повністю у фоновому режимі:

  1. Створення метаданих (State: CREATING): Після виклику UpdateTable статус індексу переходить у CREATING. DynamoDB виділяє фізичні ресурси (storage nodes) для нової партиційної структури індексу.
  2. Фаза фонового сканування та заповнення (Backfilling phase): DynamoDB автоматично запускає фоновий процес сканування основної таблиці. Кожен знайдений елемент, що містить ключові атрибути GSI, проектується та записується в індекс.
  3. Синхронізація дельти (Catch-up phase): Одночасно з заповненням історичними даними, всі нові операції запису, які виконуються клієнтами в цей момент до основної таблиці, автоматично буферизуються та реплікуються в новий індекс.
  4. Активація (State: ACTIVE): Після завершення реплікації дельти статус індексу змінюється на ACTIVE, і він стає доступним для виконання запитів читання.

Розрахунок RCU/WCU під час Backfilling

Процес створення індексу створює додаткове навантаження на пропускну здатність:

  • Фонове сканування основної таблиці споживає її власні RCU (AWS намагається виконувати це з мінімальним пріоритетом, щоб не заважати основному бізнес-трафіку).
  • Запис даних у новий індекс споживає WCU новоствореного GSI. Якщо для GSI встановлено занижене значення WCU, процес заповнення триватиме значно довше або взагалі тимчасово призупиниться.
# ── Додавання GSI SessionsByIp до існуючої таблиці UserSessions ──────────────
aws dynamodb update-table \
    --table-name UserSessions \
    --attribute-definitions \
        AttributeName=IpAddress,AttributeType=S \
        AttributeName=CreatedAt,AttributeType=S \
    --global-secondary-index-updates '[{
        "Create": {
            "IndexName": "SessionsByIp",
            "KeySchema": [
                {"AttributeName": "IpAddress", "KeyType": "HASH"},
                {"AttributeName": "CreatedAt", "KeyType": "RANGE"}
            ],
            "Projection": {
                "ProjectionType": "INCLUDE",
                "NonKeyAttributes": ["UserId", "IsActive"]
            }
        }
    }]' \
    --region eu-central-1

# ── Моніторинг статусу створення індексу ─────────────────────────────────────
aws dynamodb describe-table \
    --table-name UserSessions \
    --region eu-central-1 \
    --query "Table.GlobalSecondaryIndexes[*].{Name:IndexName, Status:IndexStatus}"
Вихідні дані опису таблиці при ініціалізації GSI
[
    {
        "Name": "SessionsByIp",
        "Status": "CREATING"
    }
]

Виконання запитів (Query) через GSI

# ── Пошук сесій за IP та часом створення ──────────────────────────────────────
aws dynamodb query \
    --table-name UserSessions \
    --index-name SessionsByIp \
    --key-condition-expression "IpAddress = :ip AND CreatedAt >= :since" \
    --expression-attribute-values '{
        ":ip":    {"S": "203.0.113.42"},
        ":since": {"S": "2025-05-25T00:00:00Z"}
    }' \
    --region eu-central-1

# ── Видалення глобального індексу GSI ─────────────────────────────────────────
aws dynamodb update-table \
    --table-name UserSessions \
    --global-secondary-index-updates '[{
        "Delete": {"IndexName": "SessionsByIp"}
    }]' \
    --region eu-central-1

Порівняльний аналіз Local Secondary Index та Global Secondary Index

Для оптимального вибору індексу під конкретний сценарій використання зверніться до порівняльної таблиці:

ХарактеристикаLocal Secondary Index (LSI)Global Secondary Index (GSI)
Partition Key (HASH)Має бути ідентичним Partition Key основної таблиціМоже бути будь-яким атрибутом таблиці
Sort Key (RANGE)Будь-який атрибут основної таблиціБудь-який атрибут основної таблиці
Момент створенняВиключно під час створення таблиціБудь-коли протягом життєвого циклу таблиці
Обмеження розміруЛіміт у 10 GB на колекцію елементів (Item Collection)Без лімітів (горизонтальне масштабування)
Пропускна здатністьВикористовує RCU/WCU основної таблиціВимагає окремого виділення RCU/WCU для індексу
Модель консистентностіStrongly Consistent або Eventually Consistent читанняВиключно Eventually Consistent читання
Штраф вибірки (Fetch Penalty)Дозволяє запитувати непроектовані атрибути (+1 RCU з основної таблиці)Не підтримує автоматичне довантаження (запит провалюється)

Вибір типу індексу (Decision Tree)

Loading diagram...
@startuml
!theme plain
skinparam backgroundColor #FAFAFA
skinparam defaultFontName "DejaVu Sans"

title Алгоритм вибору типу вторинного індексу

start

if (Потрібен додатковий ключ для нового патерну запиту?) then (так)
  if ( Partition Key має збігатися з основною таблицею?) then (так)
    if (Загальний обсяг даних на один Partition Key перевищує 10 GB?) then (так)
      :Використовувати GSI з тим самим Partition Key;
      note right : Обхід ліміту розміру Item Collection
      stop
    else (ні)
      if (Таблиця вже створена і знаходиться в експлуатації?) then (так)
        :Використовувати GSI;
        note right : LSI неможливо додати до наявної таблиці
        stop
      else (ні)
        :Використовувати LSI\n(Економія на окремій пропускній здатності + Strongly Consistent читання);
        stop
      endif
    endif
  else (ні)
    :Використовувати GSI з новим Partition Key;
    stop
  endif
else (ні)
  :Запит покривається основним ключем;
  stop
endif

@enduml

Практичний приклад: проектування індексів для E-commerce

У системах електронної комерції Single-Table Design є промисловим стандартом. Розглянемо проектування таблиці Orders, яка повинна ефективно обслуговувати чотири типи запитів.

Вимоги додатку до доступу до даних (Access Patterns):

  1. Запит 1: Отримання всіх замовлень конкретного клієнта, відсортованих за часом оформлення.
  2. Запит 2: Отримання всіх замовлень у статусі PENDING (очікують на обробку), відсортованих за часом оформлення.
  3. Запит 3: Отримання всіх замовлень, що містять конкретний продукт.
  4. Запит 4: Пошук замовлення клієнта у визначеному часовому діапазоні.

Схема проектування ключів та індексів:

  • Основний ключ (Composite PK): Partition Key — CustomerId, Sort Key — OrderDate#OrderId. Це повністю покриває Запит 1 та Запит 4 за допомогою оператора begins_with та between.
  • Індекс 1 (PendingOrdersByDate - Sparse GSI): Для Запиту 2 створюється розріджений індекс. PendingStatus використовується як HASH (заповнюється значенням PENDING лише для незавершених замовлень), а OrderDateId — як RANGE. При зміні статусу на PAID атрибут PendingStatus видаляється, і замовлення зникає з індексу.
  • Індекс 2 (OrdersByProduct - GSI): Для Запиту 3 створюється індекс, де Partition Key — ProductId_0 (ідентифікатор першого продукту у кошику), а Sort Key — OrderDateId з проекцією KEYS_ONLY.
Loading diagram...
@startuml
!theme plain
skinparam backgroundColor #FAFAFA
skinparam defaultFontName "DejaVu Sans"

title Проектування таблиці Orders та її індексів

class "Таблиця Orders" as T #E8F4FD {
  .. Первинний ключ ..
  + **CustomerId** (S) [PK]
  + **OrderDate#OrderId** (S) [SK]
  .. Атрибути ..
  - OrderId (S)
  - Status (S)
  - TotalAmount (N)
  - PendingStatus (S)  <<лише для PENDING>>
  - ProductId_0 (S) <<перший продукт>>
}

note right of T
  **GSI 1: PendingOrdersByDate (Sparse)**
  * HASH: PendingStatus
  * RANGE: OrderDateId
  * Projection: INCLUDE [CustomerId, TotalAmount]

  **GSI 2: OrdersByProduct**
  * HASH: ProductId_0
  * RANGE: OrderDateId
  * Projection: KEYS_ONLY
end note
@enduml

Практична реалізація E-commerce таблиці

# ── Створення таблиці Orders з двома GSI ──────────────────────────────────────
aws dynamodb create-table \
    --table-name Orders \
    --attribute-definitions \
        AttributeName=CustomerId,AttributeType=S \
        AttributeName=OrderDateId,AttributeType=S \
        AttributeName=PendingStatus,AttributeType=S \
        AttributeName=ProductId_0,AttributeType=S \
    --key-schema \
        AttributeName=CustomerId,KeyType=HASH \
        AttributeName=OrderDateId,KeyType=RANGE \
    --global-secondary-indexes '[
        {
            "IndexName": "PendingOrdersByDate",
            "KeySchema": [
                {"AttributeName": "PendingStatus", "KeyType": "HASH"},
                {"AttributeName": "OrderDateId",   "KeyType": "RANGE"}
            ],
            "Projection": {
                "ProjectionType": "INCLUDE",
                "NonKeyAttributes": ["CustomerId", "TotalAmount"]
            }
        },
        {
            "IndexName": "OrdersByProduct",
            "KeySchema": [
                {"AttributeName": "ProductId_0", "KeyType": "HASH"},
                {"AttributeName": "OrderDateId", "KeyType": "RANGE"}
            ],
            "Projection": {"ProjectionType": "KEYS_ONLY"}
        }
    ]' \
    --billing-mode PAY_PER_REQUEST \
    --region eu-central-1

# ── Запис нового PENDING замовлення (потрапляє в Sparse GSI) ──────────────────
aws dynamodb put-item \
    --table-name Orders \
    --item '{
        "CustomerId":    {"S": "cust-001"},
        "OrderDateId":   {"S": "2025-06-01T10:00#ord-XYZ"},
        "OrderId":       {"S": "ord-XYZ"},
        "Status":        {"S": "PENDING"},
        "PendingStatus": {"S": "PENDING"},
        "TotalAmount":   {"N": "149.99"},
        "ProductId_0":   {"S": "prod-laptop"}
    }' \
    --region eu-central-1

# ── Оплата замовлення (видалення PendingStatus -> автоматично зникає з GSI) ────
aws dynamodb update-item \
    --table-name Orders \
    --key '{"CustomerId": {"S": "cust-001"}, "OrderDateId": {"S": "2025-06-01T10:00#ord-XYZ"}}' \
    --update-expression "SET #s = :paid REMOVE PendingStatus" \
    --expression-attribute-names '{"#s": "Status"}' \
    --expression-attribute-values '{":paid": {"S": "PAID"}}' \
    --region eu-central-1

# ── Отримання активних PENDING замовлень через Sparse GSI ───────────────────────
aws dynamodb query \
    --table-name Orders \
    --index-name PendingOrdersByDate \
    --key-condition-expression "PendingStatus = :p" \
    --expression-attribute-values '{":p": {"S": "PENDING"}}' \
    --region eu-central-1
Вибірка PENDING замовлень через Sparse GSI
{
    "Items": [
        {
            "PendingStatus": { "S": "PENDING" },
            "OrderDateId": { "S": "2025-06-01T10:00#ord-XYZ" },
            "CustomerId": { "S": "cust-001" },
            "TotalAmount": { "S": "149.99" }
        }
    ],
    "Count": 1
}

Частина 3: Режими керування пропускною здатністю (Capacity Modes)

Одним із найбільш критичних архітектурних рішень при проектуванні систем на базі Amazon DynamoDB є вибір режиму керування обчислювальною ємністю. Цей параметр визначає не лише фінансову модель використання СУБД, а й механізми виділення фізичних ресурсів на рівні партицій, поведінку системи при непередбачуваних піках навантаження та стратегії автоматичного масштабування. DynamoDB пропонує два фундаментально різні підходи: Provisioned Mode (режим резервування пропускної здатності) та On-Demand Mode (режим оплати за фактичні запити).

Loading diagram...
@startuml
!theme plain
skinparam backgroundColor #FAFAFA
skinparam defaultFontName "DejaVu Sans"

title Режими обчислювальної ємності DynamoDB

rectangle "Таблиця DynamoDB" as table #E8F4FD

rectangle "Provisioned Mode (Резервування)" as prov #D5E8D4 {
  rectangle "Read Capacity Units\n(RCU)" as rcu #82B366
  rectangle "Write Capacity Units\n(WCU)" as wcu #82B366
  rectangle "Application Auto Scaling\n(Динамічне підлаштування)" as as #B8D9A9
}

rectangle "On-Demand Mode (За запитом)" as od #FFF2CC {
  rectangle "Request Read Units\n(RRU)" as rru #D6B656
  rectangle "Request Write Units\n(RWU)" as rwu #D6B656
  note right of rru : "Плата виключно за\nфактично виконані запити"
}

table --> prov : "Фіксоване виділення\nресурсів на годину"
table --> od : "Автоматичне виділення\nресурсів під трафік"

note bottom of prov
  Оплата: $/RCU/год + $/WCU/год (фіксована)
  Ризик: Обмеження (Throttling) при перевищенні лімітів
end note

note bottom of od
  Оплата: $/мільйон RRU + $/мільйон RWU
  Ризик: Висока вартість при стабільному передбачуваному навантаженні
end note
@enduml

Provisioned Mode — резервування обчислювальних ресурсів

У режимі Provisioned архітектор заздалегідь визначає обсяг обчислювальної потужності таблиці, виражений в одиницях пропускної здатності читання (RCU) та запису (WCU). AWS резервує ці ресурси на фізичному рівні (виділяючи відповідну кількість партицій та обчислювальних потужностей на вузлах збереження даних) та підтримує їх у стані постійної готовності. Оплата нараховується за кожну годину резервування, незалежно від того, чи виконувалися запити до таблиці.

Математична модель тарифікації (на прикладі регіону eu-central-1):

Вартість резервування розраховується за наступними базовими ставками:

  • 1 RCU: ≈ 0.00013 USD за годину (що становить приблизно 0.095 USD на місяць).
  • 1 WCU: ≈ 0.00065 USD за годину (що становить приблизно 0.474 USD на місяць).

При резервуванні 100 WCU та 200 RCU щомісячна вартість утримання таблиці (без врахування вартості збереження даних) становитиме:

Завантаження...
\text{Вартість} = (100 \times 0.00065 + 200 \times 0.00013) \times 24 \times 30 = 65.52\text{ USD/місяць}

Механізм обмеження пропускної здатності (Throttling)

Якщо обсяг вхідних запитів за секунду перевищує сумарно зарезервовану ємність, DynamoDB ініціює механізм захисту ресурсів, повертаючи клієнту виняток ProvisionedThroughputExceededException (HTTP статус 400).

Під капотом цей процес працює так:

  1. Генерація винятку: Запит відхиляється на рівні Request Router ще до моменту виконання операції на вузлі партиції, що запобігає перевантаженню сховища.
  2. Клієнтська обробка (Retry & Backoff): Офіційні клієнтські бібліотеки AWS SDK містять вбудовані обробники цього винятку. Вони автоматично повторюють запит, використовуючи алгоритм експоненціальної затримки з додаванням випадкового шуму (Exponential Backoff з Full Jitter). Це запобігає ефекту "лавини ретраїв" (retry storm), коли клієнти синхронно перевантажують базу даних повторними запитами.
  3. Вплив на затримку (Latency): Незважаючи на автоматичне відновлення, повторні спроби збільшують загальний час відповіді системи (Round Trip Time, RTT) для кінцевого користувача.

Механізм Burst Capacity (Імпульсна ємність)

Для згладжування короткочасних стрибків трафіку DynamoDB використовує алгоритм маркерного кошика (Token Bucket) під назвою Burst Capacity.

  • Накопичення ресурсів: Якщо реальне споживання пропускної здатності таблиці є нижчим за ліміт (u(t) < C, де C — зарезервована ємність), невикористані одиниці ємності акумулюються в спеціальному пулі (кошику).
  • Обмеження об'єму кошика: Максимальний об'єм накопичених токенів обмежений часовим інтервалом у 5 хвилин (300 секунд). Формально, максимальний запас токенів T_max дорівнює:
Завантаження...
T_{\max} = 300 \times C
  • Використання імпульсу: При різкому стрибку навантаження, що перевищує C, Request Router починає списувати токени з пулу Burst Capacity, дозволяючи додатку виконувати запити без помилок throttling.
  • Обмеження гарантій: Burst Capacity є сервісом з рівнем доступності Best Effort. AWS не гарантує надання накопиченої ємності, якщо запити концентруються на одній фізичній партиції (викликаючи Hot Partition) або якщо фізичний вузол, на якому розміщено партицію, перевантажений іншими клієнтами (noisy neighbor effect).
Loading diagram...
@startuml
!theme plain
skinparam backgroundColor #FAFAFA
skinparam defaultFontName "DejaVu Sans"

title Схема роботи механізму Burst Capacity

rectangle "Часова шкала життєвого циклу (інтервал 5 хв)" {
  rectangle "0:00–2:00 (Накопичення)\nСпоживання: 50 WCU\nРезерв: 100 WCU\nПул: +50 WCU/сек × 120 сек = 6000 токенів" as t1 #D5E8D4
  rectangle "2:01–2:30 (Імпульс)\nСпоживання: 300 WCU\nРезерв: 100 WCU\nПул покриває дефіцит 200 WCU/сек" as t2 #FFF2CC
  rectangle "2:31+ (Вичерпання)\nПул порожній\nБудь-який запис > 100 WCU\nвикликає Throttling" as t3 #F8CECC
}

t1 -right-> t2
t2 -right-> t3
@enduml

Динамічне масштабування (Auto Scaling) у Provisioned Mode

Для мінімізації ручного керування та запобігання надлишковим витратам режим Provisioned інтегрується з сервісом AWS Application Auto Scaling. Цей механізм автоматично коригує зарезервовану ємність таблиці у відповідь на динаміку реального трафіку.

Архітектура взаємодії компонентів:

  1. Моніторинг метрик: DynamoDB кожну хвилину надсилає метрики використання ємності (ConsumedReadCapacityUnits, ConsumedWriteCapacityUnits) до Amazon CloudWatch.
  2. CloudWatch Alarms: На основі конфігурації Auto Scaling створюються два триггери (Alarms): один для масштабування вгору (Scale-out), інший — для масштабування вниз (Scale-in). Вони активуються, коли середнє споживання ємності за певний проміжок часу відхиляється від встановленого цільового показника (Target Utilization, зазвичай 70%).
  3. Application Auto Scaling: При спрацьовуванні CloudWatch Alarm сервіс Auto Scaling викликає API-метод DynamoDB UpdateTable для зміни параметрів ProvisionedThroughput.
Loading diagram...
@startuml
!theme plain
skinparam backgroundColor #FAFAFA
skinparam defaultFontName "DejaVu Sans"

title Архітектура та життєвий цикл DynamoDB Auto Scaling

participant "Клієнтський додаток" as app
participant "Таблиця DynamoDB" as ddb
participant "Amazon CloudWatch" as cw
participant "Application Auto Scaling" as aas

app -> ddb : Виконання запитів (зростання трафіку)
ddb -> cw : Публікація метрики ConsumedWriteCapacityUnits
cw -> aas : Активація Alarm: Споживання > 70% від ліміту
aas -> ddb : Виклик UpdateTable (збільшення WCU)
ddb -> app : Ліміти розширено, Throttling припинено

note right of aas
  **Періоди затримки (Cooldowns):**
  * Scale-out Cooldown: 0-30 сек (миттєве розширення)
  * Scale-in Cooldown: 900 сек (захист від осциляції лімітів)
  end note
@enduml

Ковзні інтервали та запобігання осциляції (Flapping)

  • Scale-out (Масштабування вгору): Виконується максимально оперативно. Коли споживання перевищує цільове значення протягом кількох хвилин, ліміти збільшуються. Параметр Scale-out cooldown зазвичай встановлюється в 0 секунд, дозволяючи миттєву реакцію на зростання трафіку.
  • Scale-in (Масштабування вниз): Здійснюється консервативно. Зменшення лімітів ініціюється лише тоді, коли навантаження стабільно тримається нижче цільового рівня протягом тривалого часу. Параметр Scale-in cooldown за замовчуванням дорівнює 900 секундам (15 хвилинам). Це запобігає ефекту "флаппінгу" (flapping) — постійного перемикання ємності туди-сюди при короткочасних спадах трафіку, що могло б призвести до штучного дефіциту ресурсів при повторному стрибку.

On-Demand Mode — оплата за фактично оброблені запити

Режим On-Demand повністю абстрагує поняття планування ємності. Замість резервування віртуальних каналів зв'язку, користувач оплачує кожен успішно виконаний запит читання чи запису, виражений в Request Read Units (RRU) та Request Write Units (RWU). Розмірність та правила округлення для RRU/RWU є повністю аналогічними до RCU/WCU (читання блоками по 4 KB, запис блоками по 1 KB).

Вартість використання On-Demand (на прикладі регіону eu-central-1):

  • 1 мільйон RRU: ≈ 0.284 USD
  • 1 мільйон RWU: ≈ 1.42 USD

Алгоритм автоматичного масштабування під капом

On-Demand не потребує налаштування тригерів масштабування. Проте, важливо розуміти фізичні обмеження масштабованості:

  1. Історичний пік (Peak Capacity): DynamoDB гарантує миттєве обслуговування трафіку, який не перевищує подвоєне значення максимального навантаження за останні 30 хвилин. Наприклад, якщо таблиця успішно обробила пік у 10 000 RPS, вона може безпосередньо з цього моменту обробити до 20 000 RPS.
  2. Поділ партицій при перевищенні піку: Якщо навантаження перевищує 2 × Peak, DynamoDB запускає фоновий процес поділу партицій (Partition Splitting) для горизонтального розширення сховища та обчислювальних ресурсів. Під час цього процесу (який може тривати до кількох хвилин) запити, що виходять за межі подвоєного піку, можуть зазнавати тимчасового throttling.
  3. Стартові ліміти: Для абсолютно нової таблиці в режимі On-Demand встановлено ліміти у 4000 Write Requests/sec та 12 000 Read Requests/sec. Ці обмеження автоматично піднімаються в міру зростання реального трафіку.

Порівняльна характеристика режимів ємності

Критерій порівнянняProvisioned Mode (+ Auto Scaling)On-Demand Mode
Характер тарифікаціїПогодинна плата за зарезервовану ємністьПлата за фактичну кількість виконаних запитів
Реакція на різкі стрибкиЗ затримкою 5–15 хв (поки відпрацює Auto Scaling)Миттєва (у межах подвоєного історичного піку)
Фінансова ефективністьМаксимальна при стабільному, прогнозованому трафікуОптимальна при нерівномірному, непередбачуваному трафіку
Ризик виникнення ThrottlingПрисутній під час різких переходів навантаженняМайже відсутній (окрім екстремальних стрибків)
Адміністративні витратиПотребує аналізу профілю навантаження та налаштування лімітівПовний "Zero Ops" — не потребує втручання
Рекомендовано дляProduction-середовищ зі стабільними добовими цикламиMVP, розробки, тестування, систем з імпульсним трафіком

Стратегія перемикання між режимами

Loading diagram...
@startuml
!theme plain
skinparam backgroundColor #FAFAFA
skinparam defaultFontName "DejaVu Sans"

title Алгоритм вибору режиму керування ємністю

start

if (Таблиця використовується для тестування, MVP чи розробки?) then (так)
  :Вибрати On-Demand Mode\n(мінімізація адміністрування та базових витрат);
  stop
else (ні)
  if (Профіль навантаження є стабільним або прогнозованим?) then (так)
    :Вибрати Provisioned Mode + Auto Scaling\n(Оптимізація витрат, ціль: 70% утилізації);
    stop
  else (ні)
    if (Зростання трафіку перевищує 2х за 5 хвилин?) then (так)
      :Вибрати On-Demand Mode\n(Уникнення затримок масштабування Auto Scaling);
      stop
    else (ні)
      :Вибрати Provisioned Mode + Auto Scaling\nз широким діапазоном min/max ємностей;
      stop
    endif
  endif
endif
@enduml

!WARNINGОбмеження на перемикання режимів Зміна режиму з Provisioned на On-Demand (і навпаки) дозволяється не частіше ніж один раз на 24 години.

Стратегія проведення навантажувального тестування (Load Testing):

Якщо планується масштабне тестування системи або очікується маркетинговий реліз (наприклад, Black Friday), використання On-Demand для нової таблиці може викликати throttling через низький стартовий ліміт. Рекомендована стратегія:

  1. Тимчасово перевести таблицю в режим Provisioned.
  2. Встановити вручну високі показники ємності (наприклад, 10 000 WCU та 20 000 RCU), що змусить DynamoDB миттєво виділити необхідну кількість фізичних партицій ("прогріти таблицю").
  3. Провести тестування.
  4. Повернути таблицю в режим On-Demand (або знизити Provisioned ліміти) для повсякденної експлуатації.

Налаштування режимів ємності через інтерфейси керування

# ── Створення нової таблиці в режимі On-Demand (PAY_PER_REQUEST) ─────────────
aws dynamodb create-table \
    --table-name Orders \
    --attribute-definitions \
        AttributeName=CustomerId,AttributeType=S \
        AttributeName=OrderId,AttributeType=S \
    --key-schema \
        AttributeName=CustomerId,KeyType=HASH \
        AttributeName=OrderId,KeyType=RANGE \
    --billing-mode PAY_PER_REQUEST \
    --region eu-central-1

# ── Переведення існуючої таблиці в режим Provisioned з базовими лімітами ──────
aws dynamodb update-table \
    --table-name Orders \
    --billing-mode PROVISIONED \
    --provisioned-throughput ReadCapacityUnits=100,WriteCapacityUnits=50 \
    --region eu-central-1

# ── Реєстрація таблиці як об'єкта масштабування для WCU в Auto Scaling ───────
aws application-autoscaling register-scalable-target \
    --service-namespace dynamodb \
    --resource-id "table/Orders" \
    --scalable-dimension "dynamodb:table:WriteCapacityUnits" \
    --min-capacity 10 \
    --max-capacity 500 \
    --region eu-central-1

# ── Створення політики відстеження цільового використання (Target Tracking) ──
aws application-autoscaling put-scaling-policy \
    --service-namespace dynamodb \
    --resource-id "table/Orders" \
    --scalable-dimension "dynamodb:table:WriteCapacityUnits" \
    --policy-name "Orders-WCU-Scaling-Policy" \
    --policy-type TargetTrackingScaling \
    --target-tracking-scaling-policy-configuration '{
        "TargetValue": 70.0,
        "PredefinedMetricSpecification": {
            "PredefinedMetricType": "DynamoDBWriteCapacityUtilization"
        }
    }' \
    --region eu-central-1

# ── Повернення таблиці в режим оплати за фактичні запити (On-Demand) ─────────
aws dynamodb update-table \
    --table-name Orders \
    --billing-mode PAY_PER_REQUEST \
    --region eu-central-1
Вихідні дані CLI при успішному перемиканні режиму
{
    "TableDescription": {
        "TableName": "Orders",
        "TableStatus": "UPDATING",
        "BillingModeSummary": {
            "BillingMode": "PROVISIONED",
            "LastUpdateToPayPerRequestDateTime": "2026-06-03T10:00:00Z"
        },
        "ProvisionedThroughput": {
            "ReadCapacityUnits": 100,
            "WriteCapacityUnits": 50
        }
    }
}

Частина 4: DynamoDB Streams та Транзакції

DynamoDB Streams — архітектура потоків змін

DynamoDB Streams є високодоступним, розподіленим журналом транзакційних логів (Change Data Capture, CDC), який фіксує всі модифікації даних у таблиці в хронологічному порядку. Кожна операція створення (PutItem), оновлення (UpdateItem) чи видалення (DeleteItem) генерує відповідний запис зміни (Stream Record).

Архітектурні принципи та гарантії:

  1. Шардування (Shards): Потік даних автоматично розділяється на логічні фрагменти — шарди. Кожен шард відповідає певному діапазону ключів партицій і обслуговується власною обчислювальною інфраструктурою. Шарди є ефемерними: при збільшенні навантаження або обсягу даних вони автоматично розгалужуються (split), а при зменшенні — закриваються.
  2. Впорядкованість записів: DynamoDB гарантує строгу послідовність записів виключно в межах одного шарду. Якщо два записи належать до різних партицій (і відповідно, потрапляють у різні шарди), їх взаємний порядок у потоці не гарантується.
  3. Життєвий цикл (Data Retention): Всі записи в потоці зберігаються строго протягом 24 годин із моменту створення. Після цього дані видаляються без можливості відновлення.
  4. Гарантія доставки (Delivery Guarantees): Забезпечується доставка типу At-least-once (щонайменше один раз). Споживачі повинні бути готовими до обробки дублікатів записів (дедуплікація на рівні бізнес-логіки).
Loading diagram...
@startuml
!theme plain
skinparam backgroundColor #FAFAFA
skinparam defaultFontName "DejaVu Sans"

title Архітектура DynamoDB Streams та інтеграція зі споживачами

rectangle "Таблиця: Orders" as table #E8F4FD

rectangle "Потік змін (Streams)" as streams #D5E8D4 {
  rectangle "Шард 1\n(Партиції A–M)" as sh1 #82B366
  rectangle "Шард 2\n(Партиції N–Z)" as sh2 #82B366
}

rectangle "AWS Lambda\n(Event Source Mapping)" as lambda #FFF2CC
rectangle "Синхронізатор\n(InventorySync)" as lambda2 #FFF2CC

rectangle "Черга SQS\n(Сповіщення)" as sqs #F0E6FF
rectangle "OpenSearch\n(Пошуковий індекс)" as es #FFE6E6

table --> sh1 : "Запис операцій (PK: A-M)"
table --> sh2 : "Запис операцій (PK: N-Z)"

sh1 --> lambda : "Опитування (Polling) та пакетний виклик"
sh2 --> lambda : "Опитування (Polling) та пакетний виклик"
sh1 --> lambda2 : "Зчитування дельти"

lambda --> sqs : "Відправка подій"
lambda2 --> es : "Синхронізація стану"

note right of table
  * Життєвий цикл записів: 24 години
  * Доставка: At-least-once
  * Шарди автоматично масштабуються
end note
@enduml

View Types — класифікація та оцінка накладних витрат

При активації DynamoDB Streams архітектор зобов'язаний обрати тип представлення даних (View Type), який визначає обсяг інформації, що проектується у кожен запис потоку. Вибір типу безпосередньо впливає на обсяг мережевого трафіку та вартість обробки подій:

KEYS_ONLY

Опис: Записуються лише ключові атрибути елемента (Partition Key та Sort Key).
Мережеве навантаження: Мінімальне.
Використання: Оптимально, коли споживачу достатньо знати факт зміни об'єкта, а повний стан за потреби може бути завантажений з основної таблиці через GetItem (хоча це створює додаткове навантаження RCU).

NEW_IMAGE

Опис: Містить повний стан елемента після його модифікації.
Мережеве навантаження: Середнє.
Використання: Найбільш поширений варіант для побудови Read-моделей (CQRS), реплікації в Elasticsearch/Redis або відправки сповіщень про створення нових об'єктів.

OLD_IMAGE

Опис: Містить повний стан елемента до його модифікації.
Мережеве навантаження: Середнє.
Використання: Використовується в аудиторських системах для логування попереднього стану даних, а також у сценаріях компенсаційних транзакцій (Saga Pattern) для відкату змін.

NEW_AND_OLD_IMAGES

Опис: Одночасно містить стани елемента "до" та "після" змін.
Мережеве навантаження: Максимальне (додаткова серіалізація).
Використання: Необхідний для аналізу дельти (diff) змін конкретних атрибутів (наприклад, визначення факту зміни ціни або статусу замовлення).

Механізм інтеграції AWS Lambda з DynamoDB Streams

Інтеграція реалізується за допомогою сервісу AWS Lambda Event Source Mapping (ESM). Це внутрішній опитувач (Poller), який працює на стороні інфраструктури Lambda, виконуючи періодичні запити до шардів потоку через внутрішній API.

BatchSize
integer
Максимальна кількість записів, яка може бути передана в один виклик функції Lambda (діапазон 1–1000). Збільшення батчу підвищує пропускну здатність обробки, але вимагає більше оперативної пам'яті для контейнера Lambda.
StartingPosition
enum
Визначає точку старту обробки потоку. LATEST орієнтується виключно на події, що з'явилися після створення тригера. TRIM_HORIZON починає зчитування з найстаріших доступних у 24-годинному журналі записів.
BisectBatchOnFunctionError
boolean
Активує алгоритм дихотомічного (бінарного) поділу пакета при виникненні помилки обробки. ESM ділить пакет навпіл і повторює виклики для кожної частини окремо, дозволяючи локалізувати та ізолювати некоректний запис (poison pill).
MaximumRetryAttempts
integer
Визначає граничну кількість спроб повторної обробки пакета при виникненні збоїв (діапазон від 0 до 10000). Після вичерпання ліміту пакет надсилається до Dead Letter Queue (DLQ) або відкидається.
FilterCriteria
object
Задає JSON-шаблони для фільтрації подій до моменту виклику обчислювальної функції. Наприклад, дає змогу відфільтрувати події так, щоб Lambda запускалася лише при створенні записів (eventName == INSERT), економлячи обчислювальні ресурси.

Увімкнення Streams та Lambda тригера

# ── Увімкнути Streams на таблиці ─────────────────────────────────────────
aws dynamodb update-table \
    --table-name Orders \
    --stream-specification StreamEnabled=true,StreamViewType=NEW_AND_OLD_IMAGES \
    --region eu-central-1

# ── Отримати ARN потоку ───────────────────────────────────────────────────
STREAM_ARN=$(aws dynamodb describe-table \
    --table-name Orders \
    --query "Table.LatestStreamArn" \
    --output text \
    --region eu-central-1)
echo "Stream ARN: $STREAM_ARN"

# ── Підключити Lambda до потоку ───────────────────────────────────────────
aws lambda create-event-source-mapping \
    --function-name OrderStreamProcessor \
    --event-source-arn "$STREAM_ARN" \
    --batch-size 100 \
    --starting-position LATEST \
    --bisect-batch-on-function-error \
    --maximum-retry-attempts 3 \
    --filter-criteria '{"Filters": [{"Pattern": "{\"eventName\": [\"INSERT\", \"MODIFY\"]}"}]}' \
    --region eu-central-1

Транзакції в DynamoDB — внутрішня механіка та ACID-гарантії

Починаючи з 2018 року, Amazon DynamoDB підтримує виконання транзакційних запитів за допомогою інтерфейсів TransactWriteItems та TransactGetItems. Це дозволяє виконувати атомарні операції над групою до 100 елементів (або сумарним обсягом до 4 MB) в межах однієї транзакції, навіть якщо елементи розташовані в різних фізичних таблицях одного AWS-аккаунта та регіону.

Життєвий цикл транзакції та протокол Two-Phase Commit (2PC)

Під капом DynamoDB використовує адаптований протокол двофазного коміту (Two-Phase Commit, 2PC), який координується внутрішньою інфраструктурою бази даних:

  1. Фаза підготовки (Prepare Phase):
    • Координатор транзакції перевіряє ліміти та права доступу.
    • Надсилаються запити на вузли партицій, де зберігаються відповідні елементи.
    • На кожному вузлі перевіряються умови ConditionExpression. Якщо хоча б одна умова не виконується (або елемент заблоковано іншою транзакцією), транзакція негайно скасовується.
    • Елементи тимчасово блокуються для модифікації іншими конкурентними транзакціями.
  2. Фаза фіксації (Commit Phase):
    • Якщо перша фаза завершилися успішно на всіх вузлах, координатор приймає рішення про фіксацію.
    • Зміни застосовуються на фізичному рівні, створюються нові версії елементів і знімаються блокування.
    • У разі виникнення помилки на фазі підготовки ініціюється процес відкату (Rollback), і всі проміжні блокування знімаються без внесення змін до бази даних.

Специфікація операцій у TransactWriteItems:

  • Put: Запис або повна заміна елемента.
  • Update: Модифікація окремих атрибутів існуючого елемента.
  • Delete: Видалення елемента.
  • ConditionCheck: Перевірка стану елемента без його фактичної модифікації (критично для валідації зв'язаних сутностей).

!CAUTIONПодвійна вартість транзакцій (Transaction Cost Penalty) Транзакційні операції споживають вдвічі більше RCU та WCU порівняно зі стандартними запитами. Наприклад, транзакційний запис об'єкта розміром 1 KB споживає 2 WCU (замість 1 WCU), а strongly consistent читання блоку 4 KB споживає 2 RCU (замість 1 RCU).

Loading diagram...
@startuml
!theme plain
skinparam backgroundColor #FAFAFA
skinparam defaultFontName "DejaVu Sans"

title Схема транзакційного переказу балів лояльності (2PC)

actor "API Запит" as api

box "Атомарна транзакція DynamoDB" #E8F4FD
  database "Таблиця Users\n(Користувач А)" as ua
  database "Таблиця Users\n(Користувач Б)" as ub
  database "Таблиця Transactions\n(Журнал транзакцій)" as tx
end box

api -> ua : "1. ConditionCheck: balance >= 100\n(Фаза 1: Валідація лімітів)"
api -> ua : "2. Update: balance = balance - 100\n(Фаза 1: Тимчасове блокування)"
api -> ub : "3. Update: balance = balance + 100\n(Фаза 1: Тимчасове блокування)"
api -> tx : "4. Put: Запис логу переказу\n(Фаза 1: Підготовка запису)"

alt Усі перевірки успішні (Commit)
  api --> ua : "Застосування змін (Фаза 2)"
  api --> ub : "Застосування змін (Фаза 2)"
  api --> tx : "Фіксація запису (Фаза 2)"
  ua --> api : "HTTP 200 OK (Успішне завершення)"
  ub --> api : "HTTP 200 OK"
  tx --> api : "HTTP 200 OK"
else Будь-який ConditionCheck провалився (Rollback)
  ua --> api : "TransactionCanceledException\n(Усі проміжні зміни скасовано)"
  ub --> api : "TransactionCanceledException"
  tx --> api : "TransactionCanceledException"
end
@enduml

Практичний сценарій: Транзакційне оформлення замовлення (E-commerce)

При оформленні замовлення додаток повинен гарантувати атомарність трьох дій: перевірити наявність задовільної кількості товару на складі (ConditionCheck), зменшити цю кількість (Update) та безпосередньо створити картку замовлення (Put).

# ── Виконання транзакційного запису TransactWriteItems ──────────────────────
aws dynamodb transact-write-items \
    --transact-items '[
        {
            "ConditionCheck": {
                "TableName": "Inventory",
                "Key": {"ProductId": {"S": "prod-42"}},
                "ConditionExpression": "quantity >= :qty",
                "ExpressionAttributeValues": {":qty": {"N": "2"}}
            }
        },
        {
            "Update": {
                "TableName": "Inventory",
                "Key": {"ProductId": {"S": "prod-42"}},
                "UpdateExpression": "SET quantity = quantity - :qty",
                "ExpressionAttributeValues": {":qty": {"N": "2"}}
            }
        },
        {
            "Put": {
                "TableName": "Orders",
                "Item": {
                    "CustomerId":  {"S": "usr-001"},
                    "OrderId":     {"S": "ord-2026-001"},
                    "ProductId":   {"S": "prod-42"},
                    "Quantity":    {"N": "2"},
                    "Status":      {"S": "PENDING"},
                    "CreatedAt":   {"S": "2026-06-03T12:00:00Z"}
                },
                "ConditionExpression": "attribute_not_exists(OrderId)"
            }
        }
    ]' \
    --region eu-central-1

# ── Отримання кількох елементів у транзакційному режимі TransactGetItems ──────
aws dynamodb transact-get-items \
    --transact-items '[
        {"Get": {"TableName": "Orders",    "Key": {"CustomerId": {"S": "usr-001"}, "OrderId": {"S": "ord-2026-001"}}}},
        {"Get": {"TableName": "Inventory", "Key": {"ProductId": {"S": "prod-42"}}}}
    ]' \
    --region eu-central-1

Частина 5: TTL, Global Tables та Best Practices

Time to Live (TTL) — автоматичне очищення застарілих даних

Time to Live (TTL) — це вбудований безкоштовний механізм автоматичного видалення застарілих елементів з таблиці DynamoDB. Для роботи TTL розробник визначає спеціальний атрибут таблиці (наприклад, ExpiresAt), що зберігає часову мітку Unix Timestamp у секундах. Фоновий сервіс DynamoDB постійно сканує таблиці та асинхронно видаляє елементи, у яких термін придатності минув.

Особливості функціонування та архітектурний вплив:

  1. Економічність: Процес видалення елементів за допомогою TTL є абсолютно безкоштовним — він не споживає WCU (Write Capacity Units) основної таблиці, що дозволяє суттєво економити бюджет на операціях очищення.
  2. Тимчасовий лаг видалення: Видалення за допомогою TTL відбувається асинхронно. AWS гарантує очищення елемента протягом 48 годин після настання вказаного часу. У цей проміжок часу прострочений елемент все ще може відображатися в таблиці.

    !IMPORTANT Оскільки видалення не є миттєвим, клієнтський додаток повинен самостійно фільтрувати expired елементи в бізнес-сценаріях. Завжди додавайте у вирази фільтрації (FilterExpression) або бізнес-логіку умову: ExpiresAt > :currentTimestamp

  3. Інтеграція з DynamoDB Streams: Коли TTL видаляє елемент, ця подія реєструється в потоці змін Streams як подія REMOVE. Для ідентифікації того, що запис видалено саме сервісом TTL, а не користувачем, метадані події в Streams містять прапорець userIdentity.type = 'Service' та userIdentity.principalId = 'dynamodb.amazonaws.com'.
Loading diagram...
@startuml
!theme plain
skinparam backgroundColor #FAFAFA
skinparam defaultFontName "DejaVu Sans"

title Схема роботи фонового процесу TTL

rectangle "Таблиця UserSessions" as table #E8F4FD {
  rectangle "UserId: usr-001\nExpiresAt: 1748000000\n(Минулий час)" as exp #F8CECC
  rectangle "UserId: usr-002\nExpiresAt: 1890000000\n(Майбутній час)" as act #D5E8D4
}

rectangle "TTL Background Process\n(Фоновий очищувач AWS)" as worker #FFF2CC
rectangle "DynamoDB Streams" as streams #F0E6FF

worker -> exp : "1. Виявлено expired запис\n(ExpiresAt < поточний час)"
worker -> table : "2. Видалення об'єкта (без витрат WCU)"
table -> streams : "3. Подія REMOVE\n(type: 'Service')"

note right of worker
  * Латентність видалення: до 48 годин
  * Додаток має фільтрувати expired дані
end note
@enduml

Практичне налаштування та робота з TTL

# ── Активація TTL на таблиці UserSessions за атрибутом ExpiresAt ─────────────
aws dynamodb update-time-to-live \
    --table-name UserSessions \
    --time-to-live-specification Enabled=true,AttributeName=ExpiresAt \
    --region eu-central-1

# ── Запис сесії користувача з терміном дії 8 годин від поточного часу ──────────
EXPIRES=$(date -d "+8 hours" +%s 2>/dev/null || date -v+8H +%s)
aws dynamodb put-item \
    --table-name UserSessions \
    --item '{
        "UserId":    {"S": "usr-001"},
        "SessionId": {"S": "sess-new-001"},
        "CreatedAt": {"S": "2026-06-03T12:00:00Z"},
        "ExpiresAt": {"N": "'"$EXPIRES"'"},
        "IsActive":  {"BOOL": true}
    }' \
    --region eu-central-1

# ── Перевірка конфігурації TTL таблиці ──────────────────────────────────────
aws dynamodb describe-time-to-live \
    --table-name UserSessions \
    --region eu-central-1

# ── Сканування таблиці з виключенням застарілих елементів ─────────────────────
aws dynamodb scan \
    --table-name UserSessions \
    --filter-expression "ExpiresAt > :now" \
    --expression-attribute-values "{\":now\": {\"N\": \"$(date +%s)\"}}" \
    --region eu-central-1

Global Tables — мультирегіональна реплікація Active-Active

DynamoDB Global Tables — це повністю кероване рішення для забезпечення географічного розподілу та стійкості систем до відмови на рівні цілих регіонів AWS. Воно реалізує двонаправлену мультиактивну (Active-Active) реплікацію даних між обраними регіонами.

Архітектурні засади та гарантії:

  1. Асинхронна реплікація: Запис у будь-яку копію таблиці (replica) в одному регіоні автоматично реплікується в усі інші підключені регіони. Час реплікації зазвичай не перевищує однієї секунди.
  2. Конфлікти та Last Writer Wins (LWW): Оскільки запис може відбуватися одночасно в різних регіонах, при конфлікті оновлення одного елемента DynamoDB застосовує стратегію вирішення конфліктів Last Writer Wins на основі внутрішніх таймстемпів операцій.
  3. Технічні передумови:
    • Для створення Global Tables на таблиці обов'язково повинен бути увімкнений потік змін DynamoDB Streams із представленням NEW_AND_OLD_IMAGES.
    • Режими ємності RCU/WCU повинні бути ідентично налаштовані в усіх регіонах реплікації.

    !WARNING Реплікація даних споживає WCU у кожному цільовому регіоні. Якщо ви виконуєте 10 записів за секунду в регіоні eu-central-1, і у вас налаштована реплікація в us-east-1, ці записи спишуть відповідну кількість WCU в обох регіонах.

Loading diagram...
@startuml
!theme plain
skinparam backgroundColor #FAFAFA
skinparam defaultFontName "DejaVu Sans"

title Мультирегіональна Active-Active реплікація Global Tables

rectangle "eu-central-1 (Франкфурт)" as reg1 #D5E8D4 {
  database "Таблиця Orders (Replica)" as db1
  actor "Користувачі ЄС" as client1
}

rectangle "us-east-1 (Вірджинія)" as reg2 #E8F4FD {
  database "Таблиця Orders (Replica)" as db2
  actor "Користувачі США" as client2
}

rectangle "ap-southeast-1 (Сінгапур)" as reg3 #FFF2CC {
  database "Таблиця Orders (Replica)" as db3
  actor "Користувачі Азії" as client3
}

client1 --> db1 : "Локальний запис (RTT: ~10ms)"
client2 --> db2 : "Локальний запис (RTT: ~15ms)"
client3 --> db3 : "Локальний запис (RTT: ~8ms)"

db1 <--> db2 : "Асинхронна реплікація (< 1 сек)"
db2 <--> db3 : "Асинхронна реплікація (< 1 сек)"
db1 <--> db3 : "Асинхронна реплікація (< 1 сек)"
@enduml

Налаштування Global Tables

# ── Крок 1: Створення базової таблиці з увімкненим потоком Streams ───────────
aws dynamodb create-table \
    --table-name Orders \
    --attribute-definitions \
        AttributeName=CustomerId,AttributeType=S \
        AttributeName=OrderId,AttributeType=S \
    --key-schema \
        AttributeName=CustomerId,KeyType=HASH \
        AttributeName=OrderId,KeyType=RANGE \
    --billing-mode PAY_PER_REQUEST \
    --stream-specification StreamEnabled=true,StreamViewType=NEW_AND_OLD_IMAGES \
    --region eu-central-1

# ── Крок 2: Додавання реплік у регіони us-east-1 та ap-southeast-1 ───────────
aws dynamodb update-table \
    --table-name Orders \
    --replica-updates '[
        {"Create": {"RegionName": "us-east-1"}},
        {"Create": {"RegionName": "ap-southeast-1"}}
    ]' \
    --region eu-central-1

# ── Отримання статусу реплікації ─────────────────────────────────────────────
aws dynamodb describe-table \
    --table-name Orders \
    --query "Table.Replicas" \
    --region eu-central-1

Best Practices: Ефективне проектування схем та запитів

Для забезпечення масштабованості, передбачуваності затримок (Latency) та мінімізації фінансових витрат при роботі з Amazon DynamoDB розробник повинен дотримуватися наступних кращих практик:

1. Уникнення проблеми гарячих партицій (Hot Partitions)

Якщо значення Partition Key розподілені нерівномірно (наприклад, більшість запитів надсилається до одного ключа Status = PENDING), вся ємність RCU/WCU для конкретної фізичної партиції може бути вичерпана, що викличе Throttling для інших клієнтів.

Рішення — Розподіл записів за допомогою солі (Write Sharding / Salting): До Partition Key при записі додається випадковий суфікс (сіль) із фіксованого діапазону:

# Запис: PK = "STATUS#PENDING#" + Random(1, N)
# Приклад: "STATUS#PENDING#3", "STATUS#PENDING#12"

При читанні додаток виконує N паралельних запитів (Query) для кожного можливого суфікса та об'єднує результати в пам'яті.

2. Query проти Scan

  • Scan: Повністю зчитує всю таблицю, проходячи по всіх фізичних партиціях. Це операція зі складністю O(N). Вона споживає величезну кількість RCU, блокує інші запити і повинна використовуватися виключно у фонових аналітичних задачах.
  • Query: Виконує локальний пошук у межах конкретної партиції за Partition Key (складність O(1) або O(log N)). Це найбільш ефективний спосіб читання даних, який мінімізує витрати RCU.

3. Оптимістичне блокування за допомогою Conditional Writes

Оскільки DynamoDB не підтримує класичні блокування рядків на час транзакції, для запобігання перезапису конкурентними процесами використовується оптимістичне блокування (Optimistic Locking) через атрибут версійності та ConditionExpression.

# Оновлення запису продукту лише якщо його версія збігається з очікуваною
aws dynamodb update-item \
    --table-name Products \
    --key '{"ProductId": {"S": "prod-42"}}' \
    --update-expression "SET price = :newPrice, version = :newVer" \
    --condition-expression "version = :expectedVer" \
    --expression-attribute-values '{
        ":newPrice": {"N": "89.99"},
        ":newVer": {"N": "3"},
        ":expectedVer": {"N": "2"}
    }' \
    --region eu-central-1

Якщо інший потік змінив запис першим (версія стала 3), операція поверне ConditionalCheckFailedException, сигналізуючи про необхідність повторного зчитування та спроби оновлення.

Частина 6: Інтеграція з .NET (AWS SDK)

Офіційне AWS SDK для .NET (AWSSDK.DynamoDBv2) є основним інструментом для взаємодії з Amazon DynamoDB у середовищі C# та .NET. Воно розроблене з урахуванням високої продуктивності, асинхронності та підтримує різноманітні рівні абстракції, дозволяючи розробникам балансувати між швидкістю розробки та низькорівневим контролем над мережевим трафіком і структурами даних.

У цій частині ми детально вивчимо всі складові частини SDK, починаючи від інсталяції та життєвого циклу клієнтів у контейнері DI, завершуючи складними транзакціями та обробкою потоків змін у безсерверних функціях AWS Lambda.


1. Архітектурні рівні AWS SDK для .NET

AWS SDK надає три взаємодоповнюючі моделі програмування. Кожна з них орієнтована на конкретні сценарії використання:

Loading diagram...
graph TD
    Client["AmazonDynamoDBClient<br/>(Low-Level API)"]
    DocModel["Table & Document<br/>(Document Model)"]
    OPM["DynamoDBContext<br/>(Object Persistence Model / ORM)"]

    Client -->|Керує низькорівневими запитами| DynamoDB[(Amazon DynamoDB)]
    DocModel -->|Обгортає клієнт| Client
    OPM -->|Використовує під капотом| Client
    OPM -.->|Може інтегруватися| DocModel

Low-Level API

Клас: AmazonDynamoDBClient
Суть: Найближчий до HTTP-протоколу рівень. Усі типу даних та структури передаються у вигляді сирих словників Dictionary<string, AttributeValue>.
Переваги: Повний контроль, нульовий оверхед, доступ до 100% можливостей API (транзакції, batch-операції, тюнінг запитів).

Document Model

Клас: Table + Document
Суть: Проміжний безсхемний (schemaless) рівень. Замість явного використання AttributeValue робота відбувається з динамічними об'єктами Document, що спрощує зчитування та запис.
Переваги: Швидка JSON-серіалізація, легка робота з динамічними даними без опису C# класів.

Object Persistence Model

Клас: DynamoDBContext
Суть: Декларативний ORM-подібний рівень. SDK автоматично перетворює POCO (Plain Old CLR Objects) на записи таблиць на основі атрибутів класу.
Переваги: Декларативне проектування, мінімум коду, висока типізація та безпека типів на рівні компіляції.

Порівняльна матриця рівнів абстракції

ХарактеристикаLow-Level APIDocument ModelObject Persistence Model (OPM)
Швидкість розробкиНизька (багато бойлерплейту)СередняВисока (декларативний підхід)
ПродуктивністьМаксимальна (без додаткового мапінгу)ВисокаЗлегка знижена через рефлексію
ТипізаціяСловники та AttributeValueДинамічні типи (Document, Primitive)Строга типізація (POCO класи)
Контроль запитівПовний контроль над схемами та фільтрамиЧастковий контрольОбмежений абстракціями контексту
ТранзакціїПідтримуються повністюНе підтримуються напрямуПідтримуються обмежено
Ідеально для...Системних утиліт, складних запитівJSON API, динамічних схемБізнес-сутностей та CRUD-сервісів

2. Встановлення, конфігурація та Dependency Injection

Перед початком роботи необхідно підключити відповідні інструменти з екосистеми NuGet.

Крок 1: Встановлення NuGet-пакетів

Для роботи з клієнтом та DI у сучасних .NET 8+ додатках потрібні такі пакети:

# Основна бібліотека клієнта DynamoDB
dotnet add package AWSSDK.DynamoDBv2

# Розширення для інтеграції з .NET Core Dependency Injection
dotnet add package AWSSDK.Extensions.NETCore.Setup

Крок 2: Налаштування файлу конфігурації appsettings.json

Параметри доступу до хмари AWS зазвичай зберігаються у конфігураційному файлі. Розділ "AWS" зчитується автоматично розширенням SDK:

{
  "AWS": {
    "Region": "eu-central-1",
    "Profile": "default"
  }
}

!NOTE У хмарному середовищі AWS (наприклад, у контейнерах ECS/EKS або Lambda-функціях) вказувати Profile не потрібно. SDK автоматично отримає тимчасові Credentials через механізм IAM Roles for Tasks або Execution Role.

Крок 3: Реєстрація у контейнері Dependency Injection (DI)

Для правильного керування життєвим циклом підключень клієнтські сервіси реєструються у Program.cs:

using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.DataModel;

var builder = WebApplication.CreateBuilder(args);

// 1. Зчитування та реєстрація опцій AWS
var awsOptions = builder.Configuration.GetAWSOptions();
builder.Services.AddDefaultAWSOptions(awsOptions);

// 2. Реєстрація IAmazonDynamoDB (Singleton за замовчуванням)
builder.Services.AddAWSService<IAmazonDynamoDB>();

// 3. Реєстрація IDynamoDBContext (ORM контекст - зазвичай Scoped)
builder.Services.AddScoped<IDynamoDBContext, DynamoDBContext>();

!IMPORTANT Клієнт AmazonDynamoDBClient реалізує IDisposable та утримує внутрішній пул з'єднань (HttpClient). Його обов'язково потрібно реєструвати як Singleton, оскільки створення нового клієнта на кожен запит призведе до вичерпання TCP-сокетів під навантаженням (Socket Exhaustion). Контекст DynamoDBContext є легкою обгорткою над клієнтом і реєструється як Scoped або Transient.

Крок 4: Локальна розробка та перевизначення ServiceURL

При використанні локального емулятора (наприклад, LocalStack або DynamoDB Local) необхідно обійти стандартну маршрутизацію AWS:

if (builder.Environment.IsDevelopment())
{
    builder.Services.AddSingleton<IAmazonDynamoDB>(sp =>
    {
        var config = new AmazonDynamoDBConfig
        {
            ServiceURL = "http://localhost:8000", // Стандартний порт DynamoDB Local
            UseHttp = true
        };
        return new AmazonDynamoDBClient(config);
    });
}

Конфігурація клієнта (AmazonDynamoDBConfig)

Клас AmazonDynamoDBConfig (успадкований від AmazonServiceClientConfig) дозволяє гнучко налаштувати поведінку HTTP-клієнта та правила взаємодії з AWS.

ServiceURL
string
Дозволяє перевизначити базову URL-адресу для запитів. Корисно для підключення до локальних емуляторів (LocalStack, DynamoDB Local).
RegionEndpoint
RegionEndpoint
Вказує географічний регіон AWS (наприклад, RegionEndpoint.EUCentral1), до якого надсилатимуться запити, якщо не вказано ServiceURL.
UseHttp
bool
Якщо встановлено true, SDK використовуватиме протокол HTTP замість HTTPS (корисно для локального тестування).
MaxErrorRetry
int
Максимальна кількість автоматичних повторних спроб (retries) при отриманні тимчасових помилок від DynamoDB або проблем із мережею.
ConnectionLimit
int
Максимальна кількість одночасних TCP-з'єднань, які HTTP-клієнт може відкривати до DynamoDB. Рекомендується збільшувати для високонавантажених систем.
Timeout
TimeSpan
Максимальний час очікування відповіді на HTTP-запит.

3. Low-Level API: Робота на рівні байтів та протоколу

Low-Level API надає прямий доступ до викликів JSON-інтерфейсу DynamoDB через клас AmazonDynamoDBClient. Основною структурою даних тут є AttributeValue, яка реалізує паттерн Union Type для опису типів колонок БД.

Ключовий клас: AmazonDynamoDBClient

Це центральний клас низькорівневого доступу, який безпосередньо надсилає HTTP-запити до AWS API. Усі методи є асинхронними та приймають відповідний об'єкт *Request.

GetItemAsync
Method
Отримує один елемент за його точним первинним ключем (HASH або HASH+RANGE). Приймає GetItemRequest та повертає GetItemResponse.
PutItemAsync
Method
Записує новий елемент або повністю перезаписує існуючий. Приймає PutItemRequest.
UpdateItemAsync
Method
Модифікує окремі атрибути існуючого елемента без повного перезапису сутності. Підтримує умовні вирази. Приймає UpdateItemRequest.
DeleteItemAsync
Method
Видаляє елемент за первинним ключем. Приймає DeleteItemRequest.
QueryAsync
Method
Шукає елементи в межах однієї партиції (Partition Key). Дозволяє фільтрувати за сортувальним ключем (Sort Key). Приймає QueryRequest.
ScanAsync
Method
Повністю сканує всю таблицю, проходячи по всіх фізичних партиціях. Вкрай неефективний для великих таблиць. Приймає ScanRequest.
BatchGetItemAsync
Method
Пакетне зчитування до 100 елементів за один виклик. Приймає BatchGetItemRequest.
BatchWriteItemAsync
Method
Пакетний запис або видалення до 25 елементів за один виклик. Приймає BatchWriteItemRequest.
TransactGetItemsAsync
Method
Транзакційне читання кількох елементів з гарантіями ACID. Приймає TransactGetItemsRequest.
TransactWriteItemsAsync
Method
Транзакційний запис/оновлення/видалення з гарантіями ACID (до 100 елементів). Приймає TransactWriteItemsRequest.

Опис властивостей класу AttributeValue

Клас AttributeValue містить властивості для кожного підтримуваного типу даних DynamoDB. На рівні SDK це виглядає так:

S
string
Рядковий тип даних (String). Зберігає текстові дані (наприклад, UUID, дати у форматі ISO 8601).
N
string
Числовий тип даних (Number). Передається виключно як рядок C# для запобігання втрати точності чисел з плаваючою крапкою.
BOOL
bool
Логічний тип (Boolean). Приймає true або false.
B
MemoryStream
Бінарні дані (Binary). Приймає потік байтів MemoryStream (наприклад, для серіалізованих protobuf-файлів).
SS
List<string>
Набір рядків (String Set). Список унікальних текстових значень.
NS
List<string>
Набір чисел (Number Set). Список унікальних чисел у форматі рядків C#.
BS
List<MemoryStream>
Набір бінарних даних (Binary Set).
M
Dictionary<string, AttributeValue>
Карта/Словник (Map). Дозволяє створювати глибокі ієрархічні структури (JSON-подібні).
L
List<AttributeValue>
Список (List). Впорядкований масив елементів будь-яких типів.
NULL
bool
Визначає пусте значення (Null).

CRUD-операції через Low-Level API

Розглянемо приклад реалізації записів, зчитування та оновлення через прямі HTTP-запити:

using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.Model;

public class LowLevelRepository
{
    private readonly IAmazonDynamoDB _client;
    private const string TableName = "Products";

    public LowLevelRepository(IAmazonDynamoDB client) => _client = client;

    // ── PutItem: Запис нового елемента ──────────────────────────────────────────
    public async Task CreateProductAsync(string categoryId, string sku, string name, decimal price)
    {
        var request = new PutItemRequest
        {
            TableName = TableName,
            Item = new Dictionary<string, AttributeValue>
            {
                { "CategoryId", new AttributeValue { S = categoryId } },
                { "Sku", new AttributeValue { S = sku } },
                { "Name", new AttributeValue { S = name } },
                { "Price", new AttributeValue { N = price.ToString("F2") } },
                { "IsAvailable", new AttributeValue { BOOL = true } }
            }
        };

        await _client.PutItemAsync(request);
    }

    // ── GetItem: Точкове зчитування елемента за HASH та RANGE ─────────────────────
    public async Task<Dictionary<string, AttributeValue>?> GetProductAsync(string categoryId, string sku)
    {
        var request = new GetItemRequest
        {
            TableName = TableName,
            Key = new Dictionary<string, AttributeValue>
            {
                { "CategoryId", new AttributeValue { S = categoryId } },
                { "Sku", new AttributeValue { S = sku } }
            },
            ConsistentRead = true // Strongly Consistent Read
        };

        var response = await _client.GetItemAsync(request);
        return response.IsItemSet ? response.Item : null;
    }

    // ── UpdateItem: Оновлення окремого атрибута за виразом (Update Expression) ───
    public async Task UpdatePriceAsync(string categoryId, string sku, decimal newPrice)
    {
        var request = new UpdateItemRequest
        {
            TableName = TableName,
            Key = new Dictionary<string, AttributeValue>
            {
                { "CategoryId", new AttributeValue { S = categoryId } },
                { "Sku", new AttributeValue { S = sku } }
            },
            // Вираз оновлення: встановлюємо нову ціну та дату
            UpdateExpression = "SET Price = :newPrice, UpdatedAt = :ts",
            ExpressionAttributeValues = new Dictionary<string, AttributeValue>
            {
                { ":newPrice", new AttributeValue { N = newPrice.ToString("F2") } },
                { ":ts", new AttributeValue { S = DateTime.UtcNow.ToString("O") } }
            }
        };

        await _client.UpdateItemAsync(request);
    }
}

Використання виразів (Expressions) у запитах

При читанні великих обсягів даних за допомогою QueryRequest або ScanRequest важливо використовувати вирази для мінімізації споживання RCU та фільтрації на боці сервера:

  • KeyConditionExpression: Визначає критерії відбору за первинним ключем (обов'язково вказувати HASH-ключ).
  • FilterExpression: Додатковий фільтр, який застосовується до знайдених даних після зчитування з диска, але перед поверненням клієнту (не економить RCU, але зменшує мережевий трафік).
  • ProjectionExpression: Перелік атрибутів, які необхідно повернути (аналог SELECT column1, column2). Економить пропускну здатність мережі.
  • ExpressionAttributeNames (словник з #): Використовується для екранування зарезервованих слів DynamoDB (таких як Status, Name, User, Timestamp).
  • ExpressionAttributeValues (словник з :): Передає значення параметрів у вирази.

Приклад побудови запиту із зарезервованими іменами атрибутів:

var queryRequest = new QueryRequest
{
    TableName = "Products",
    KeyConditionExpression = "CategoryId = :catId AND Sku >= :minSku",
    FilterExpression = "#status = :statusVal AND Price < :maxPrice",
    ProjectionExpression = "Sku, #nameAttr, Price",
    ExpressionAttributeNames = new Dictionary<string, string>
    {
        { "#status", "Status" }, // Зарезервоване слово
        { "#nameAttr", "Name" }   // Зарезервоване слово
    },
    ExpressionAttributeValues = new Dictionary<string, AttributeValue>
    {
        { ":catId", new AttributeValue { S = "Electronics" } },
        { ":minSku", new AttributeValue { S = "SKU-1000" } },
        { ":statusVal", new AttributeValue { S = "ACTIVE" } },
        { ":maxPrice", new AttributeValue { N = "500.00" } }
    }
};

var response = await _client.QueryAsync(queryRequest);

4. Document Model: Робота з неструктурованими документами

Document Model — це проміжний рівень абстракції, який надає класи Table та Document для зручної роботи зі схемами без необхідності опису об'єктів AttributeValue.

Ключові класи моделі документів

Для роботи з даними використовуються три основні класи:

Table
Class
Представляє таблицю DynamoDB. Містить методи для CRUD-операцій на рівні документів. Створюється через статичний фабричний метод Table.LoadTable(client, tableName).
Document
Class
Представляє окремий запис як колекцію пар "ключ-значення" (без жорсткої схеми). Самостійно мапить типи .NET (string, decimal, bool) на типи DynamoDB.
Search
Class
Клас-ітератор для обробки результатів запитів Query та Scan з підтримкою пагінації.

Основні методи класу Table

GetItemAsync
Method
Зчитує Document за первинним ключем.
PutItemAsync
Method
Зберігає або перезаписує Document у таблиці.
UpdateItemAsync
Method
Оновлює частину атрибутів документа.
DeleteItemAsync
Method
Видаляє документ за ключем.
Query
Method
Виконує пошук за ключем партиції та сортувальним критеріями. Повертає об'єкт Search.
Scan
Method
Виконує повне сканування таблиці. Повертає об'єкт Search.

Допоміжні методи класу Document

FromJson(string json)
Static Method
Створює об'єкт Document з валідного JSON-рядка.
ToJson()
Method
Серіалізує поточний Document у JSON-рядок.
As[Type]()
Conversion Methods
Група методів приведення значень атрибутів: AsString(), AsInt(), AsDecimal(), AsBoolean(), AsDouble().

Методи класу Search (Пагінація)

GetNextSetAsync()
Method
Зчитує наступну сторінку результатів з БД.
GetRemainingAsync()
Method
Зчитує всі залишені записи, що відповідають запиту, автоматично роблячи послідовні запити до БД.
IsDone
Property (bool)
Повертає true, якщо в БД більше немає сторінок для завантаження.

CRUD-операції в моделі документів

using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.DocumentModel;

public class DocumentModelRepository
{
    private readonly Table _table;

    public DocumentModelRepository(IAmazonDynamoDB client)
    {
        // Завантаження метаданих таблиці при ініціалізації
        _table = Table.LoadTable(client, "Products");
    }

    public async Task PutDocumentAsync(string categoryId, string sku, string name, decimal price, List<string> tags)
    {
        var productDoc = new Document();
        productDoc["CategoryId"] = categoryId; // Автоматична конвертація в AttributeValue (S)
        productDoc["Sku"] = sku;
        productDoc["Name"] = name;
        productDoc["Price"] = price;          // Автоматична конвертація в (N)
        productDoc["Tags"] = tags;            // Автоматична конвертація в String Set (SS)
        productDoc["CreatedAt"] = DateTime.UtcNow;

        await _table.PutItemAsync(productDoc);
    }

    public async Task<Document?> GetDocumentAsync(string categoryId, string sku)
    {
        // Метод LoadItemAsync автоматично будує запит GetItem
        return await _table.GetItemAsync(categoryId, sku);
    }
}

Інтеграція з JSON-даними

Однією з найсильніших переваг моделі документів є підтримка прямого імпорту та експорту JSON-структур за допомогою методів Document.FromJson та Document.ToJson:

// ── Імпорт: Перетворення JSON-рядка безпосередньо у запис таблиці ─────────────
string rawJson = """
{
  "CategoryId": "Books",
  "Sku": "BOOK-9921",
  "Name": "Clean Architecture",
  "Details": {
    "Author": "Robert C. Martin",
    "Pages": 432
  }
}
""";

Document document = Document.FromJson(rawJson);
await _table.PutItemAsync(document);

// ── Експорт: Перетворення зчитаного документа у JSON-рядок ────────────────────
Document fetchedDoc = await _table.GetItemAsync("Books", "BOOK-9921");
string jsonOutput = fetchedDoc.ToJson(); 
// jsonOutput міститиме стандартний валідний JSON

Виконання запитів через Search Helper

Для ітерації по вибірці даних використовується клас Search, який підтримує ледачу підвантаження результатів сторінками:

var filter = new QueryFilter("CategoryId", QueryOperator.Equal, "Books");
filter.AddCondition("Price", QueryOperator.LessThan, 50.00m);

var search = _table.Query(filter);

while (!search.IsDone)
{
    // Отримання наступної сторінки записів (Pagination)
    List<Document> page = await search.GetNextSetAsync();
    foreach (var doc in page)
    {
        Console.WriteLine($"{doc["Name"]} - {doc["Price"].AsDecimal()}");
    }
}

5. Object Persistence Model: Декларативний ORM

Object Persistence Model представляє найвищий рівень абстракції (ORM). Він використовує об'єкт DynamoDBContext для мапінгу стандартних POCO-класів на таблиці за допомогою системи декларативних C#-атрибутів.

Ключовий клас: DynamoDBContext

Це основний робочий інструмент для ORM-мапінгу. Його інтерфейс IDynamoDBContext надає методи для збереження та завантаження POCO-об'єктів.

LoadAsync<T>
Method
Завантажує запис з БД та десеріалізує його в об'єкт типу T за ключами.
SaveAsync<T>
Method
Зберігає об'єкт типу T у базі даних (виконує Upsert). Автоматично інкрементує версію сутності, якщо налаштовано [DynamoDBVersion].
DeleteAsync<T>
Method
Видаляє об'єкт з бази даних за первинним ключем або за допомогою самого примірника сутності.
QueryAsync<T>
Method
Виконує типізований запис Query за Partition Key. Повертає AsyncSearch<T>.
ScanAsync<T>
Method
Виконує типізоване сканування таблиці. Повертає AsyncSearch<T>.
CreateBatchWrite<T>
Method
Створює пакетний запис для групових операцій над сутностями типу T.
CreateBatchGet<T>
Method
Створює пакетний запит для зчитування списку об'єктів типу T за списком первинних ключів.
FromDocument<T>
Method
Конвертує слабкотипізований Document у строготипізований POCO-об'єкт T.
ToDocument<T>
Method
Конвертує POCO-об'єкт T у слабкотипізований Document.

Локальне перевизначення конфігурації (DynamoDBOperationConfig)

При використанні DynamoDBContext ви можете динамічно змінювати параметри виконання окремих CRUD-операцій за допомогою об'єкта DynamoDBOperationConfig.

IndexName
string
Вказує назву індексу (GSI або LSI), який необхідно використовувати для виконання операцій Query або Scan.
ConsistentRead
bool
Визначає, чи використовувати Strongly Consistent читання. Зверніть увагу: GSI не підтримують Strongly Consistent читання.
OverrideTableName
string
Дозволяє динамічно спрямувати запит до іншої таблиці (наприклад, для шардингу або мультитенантності), ігноруючи ім'я, вказане в атрибуті [DynamoDBTable].
IgnoreNullValues
bool
Якщо встановлено true, властивості POCO-класу зі значенням null не перезаписуватимуть існуючі значення в БД при використанні SaveAsync.
SkipVersionCheck
bool
Дозволяє тимчасово вимкнути перевірку версії (Optimistic Locking), ігноруючи атрибут [DynamoDBVersion].

Атрибути мапування даних OPM

Для опису сутностей у просторі назв Amazon.DynamoDBv2.DataModel використовуються такі атрибути:

[DynamoDBTable]
Class Attribute required
Вказує назву цільової таблиці DynamoDB.
[DynamoDBHashKey]
Property Attribute required
Визначає Partition Key (HASH).
[DynamoDBRangeKey]
Property Attribute required
Визначає Sort Key (RANGE).
[DynamoDBProperty]
Property Attribute required
Вказує відповідність властивості та колонки. Дозволяє налаштовувати назву атрибута в БД та задавати кастомні конвертери (Converter).
[DynamoDBIgnore]
Property Attribute required
Повністю виключає поле з процесів серіалізації.
[DynamoDBVersion]
Property Attribute required
Задіює Оптимістичне блокування (Optimistic Locking). Поле автоматично інкрементується при записі та викликає виняток, якщо версія в БД була змінена іншим потоком.
[DynamoDBGlobalSecondaryIndexHashKey]
Property Attribute required
Визначає HASH-ключ для вказаного глобального індексу (GSI).
[DynamoDBGlobalSecondaryIndexRangeKey]
Property Attribute required
Визначає RANGE-ключ для глобального індексу (GSI).
[DynamoDBLocalSecondaryIndexRangeKey]
Property Attribute required
Визначає RANGE-ключ локального індексу (LSI).

Проектування POCO-моделі даних

Нижче наведено приклад опису складної моделі даних з підтримкою GSI, оптимістичного блокування та вкладеного об'єкта, серіалізованого через кастомний конвертер:

using Amazon.DynamoDBv2.DataModel;

[DynamoDBTable("Catalog")]
public class CatalogItem
{
    [DynamoDBHashKey("CategoryId")] // Первинний HASH
    public string CategoryId { get; set; } = string.Empty;

    [DynamoDBRangeKey("Sku")] // Первинний RANGE
    public string Sku { get; set; } = string.Empty;

    [DynamoDBProperty("Title")]
    public string Title { get; set; } = string.Empty;

    [DynamoDBProperty("Price")]
    public decimal Price { get; set; }

    // Глобальний індекс: HASH-ключ для індексу "Supplier-index"
    [DynamoDBGlobalSecondaryIndexHashKey("Supplier-index", AttributeName = "SupplierId")]
    public string SupplierId { get; set; } = string.Empty;

    // Автоматичне оптимістичне блокування версії запису
    [DynamoDBVersion]
    public int? Version { get; set; }

    // Вкладений складний об'єкт із кастомним конвертером в JSON String
    [DynamoDBProperty("SpecSheet", Converter = typeof(SpecsJsonConverter))]
    public TechnicalSpecs Specs { get; set; } = new();

    [DynamoDBIgnore]
    public string MemoryCache { get; set; } = string.Empty;
}

public class TechnicalSpecs
{
    public string Manufacturer { get; set; } = string.Empty;
    public string WarrantyMonths { get; set; } = string.Empty;
}

Кастомний конвертер типів (IPropertyConverter)

Для збереження складних об'єктів або списків у вигляді одного атрибута (наприклад, JSON-рядка) необхідно імплементувати інтерфейс IPropertyConverter:

using System.Text.Json;
using Amazon.DynamoDBv2.DataModel;
using Amazon.DynamoDBv2.DocumentModel;

public class SpecsJsonConverter : IPropertyConverter
{
    // Серіалізація об'єкта C# у DynamoDBEntry
    public DynamoDBEntry ToEntry(object value)
    {
        if (value is not TechnicalSpecs specs)
            return new DynamoDBNull();

        string json = JsonSerializer.Serialize(specs);
        return new Primitive(json); // Зберігаємо як звичайний рядок
    }

    // Десеріалізація з DynamoDBEntry в об'єкт C#
    public object FromEntry(DynamoDBEntry entry)
    {
        var primitive = entry as Primitive;
        if (primitive == null || primitive.Value is not string json)
            return new TechnicalSpecs();

        return JsonSerializer.Deserialize<TechnicalSpecs>(json) ?? new TechnicalSpecs();
    }
}

Методи інтерфейсу IPropertyConverter

ToEntry(object value)
Method
Серіалізує властивість C# класу в об'єкт DynamoDBEntry (наприклад, Primitive, Document, DynamoDBNull або DynamoDBList).
FromEntry(DynamoDBEntry entry)
Method
Десеріалізує об'єкт DynamoDBEntry з бази даних у вихідний тип властивості C#.

Реалізація CRUD Репозиторію через OPM

using Amazon.DynamoDBv2.DataModel;

public class DynamoDBCatalogRepository
{
    private readonly IDynamoDBContext _context;

    public DynamoDBCatalogRepository(IDynamoDBContext context) => _context = context;

    // ── Load: Точкове зчитування (GetItem) ──────────────────────────────────────
    public async Task<CatalogItem?> GetByIdAsync(string categoryId, string sku)
    {
        return await _context.LoadAsync<CatalogItem>(categoryId, sku);
    }

    // ── Save: Збереження або перезапис (Upsert) ─────────────────────────────────
    public async Task SaveItemAsync(CatalogItem item)
    {
        // Метод SaveAsync автоматично аналізує [DynamoDBVersion].
        // Якщо запис існує, версія звіриться і збільшиться на 1.
        // При конфлікті виникне AmazonDynamoDBException із кодом помилки ConditionalCheckFailedException.
        try
        {
            await _context.SaveAsync(item);
        }
        catch (AmazonDynamoDBException ex) when (ex.ErrorCode == "ConditionalCheckFailedException")
        {
            throw new InvalidOperationException("Запис був змінений іншим користувачем. Оновіть дані.", ex);
        }
    }

    // ── Delete: Видалення сутності ──────────────────────────────────────────────
    public async Task DeleteItemAsync(string categoryId, string sku)
    {
        await _context.DeleteAsync<CatalogItem>(categoryId, sku);
    }

    // ── Query через GSI: Зчитування за альтернативним індексом ───────────────────
    public async Task<IEnumerable<CatalogItem>> GetBySupplierAsync(string supplierId)
    {
        var config = new DynamoDBOperationConfig
        {
            IndexName = "Supplier-index",
            ConsistentRead = false // На GSI допускається лише Eventually Consistent Read
        };

        // QueryAsync повертає об'єкт AsyncSearch<T>, який виступає ітератором
        AsyncSearch<CatalogItem> search = _context.QueryAsync<CatalogItem>(supplierId, config);
        
        // Зчитуємо всі сторінки даних, які залишилися
        return await search.GetRemainingAsync();
    }
}

6. Атомарні транзакції та пакетна обробка

Для забезпечення максимальної продуктивності та надійності даних SDK надає механізми пакетної роботи та транзакційності на рівні ACID.

Пакетні операції (Batch Operations)

Пакетний запис (BatchWrite) та зчитування (BatchGet) дозволяють об'єднати до 25 операцій запису/видалення та до 100 операцій читання в один HTTP-запит. Це значно знижує RTT (Round Trip Time).

// Пакетний запис сутностей через ORM Context
public async Task SaveBatchAsync(List<CatalogItem> items)
{
    var batchWrite = _context.CreateBatchWrite<CatalogItem>();
    
    // Додаємо елементи до пакету
    batchWrite.AddPutItems(items);
    
    // Можна комбінувати з видаленням
    batchWrite.AddDeleteKey("Books", "BOOK-DELETE-1");

    await batchWrite.ExecuteAsync();
}

!WARNING При використанні пакетних запитів DynamoDB може повернути частину елементів як необроблені (UnprocessedItems / UnprocessedKeys) у випадку вичерпання виділеної WCU/RCU ліміту. Низькорівневий API вимагає ручної обробки та повторних спроб у циклі. IDynamoDBContext.ExecuteAsync() намагається обробити частину ретриів самостійно, але у критичних сценаріях розробник має відслідковувати статус відповіді.

Атомарні ACID Транзакції

Транзакції гарантують повне виконання (All-or-Nothing) до 100 операцій (або 4 МБ об'єму). Вони реалізуються виключно через Low-Level API клієнта AmazonDynamoDBClient.

Розглянемо класичний приклад переказу коштів або замовлення товару з валідацією залишків на складі:

using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.Model;

public class TransactionService
{
    private readonly IAmazonDynamoDB _client;

    public TransactionService(IAmazonDynamoDB client) => _client = client;

    public async Task ProcessOrderAsync(string customerId, string productId, int qty, decimal price)
    {
        string orderId = $"ORD-{Guid.NewGuid():N}";

        var request = new TransactWriteItemsRequest
        {
            TransactItems = new List<TransactWriteItem>
            {
                // 1. Валідація та оновлення залишків на складі (Умова: склад має достатньо товару)
                new()
                {
                    Update = new Update
                    {
                        TableName = "Warehouse",
                        Key = new Dictionary<string, AttributeValue>
                        {
                            { "ProductId", new AttributeValue { S = productId } }
                        },
                        UpdateExpression = "SET Stock = Stock - :qty",
                        ConditionExpression = "Stock >= :qty", // Перевірка умови
                        ExpressionAttributeValues = new Dictionary<string, AttributeValue>
                        {
                            { ":qty", new AttributeValue { N = qty.ToString() } }
                        }
                    }
                },
                // 2. Створення нового запису замовлення (Умова: таке замовлення ще не існує)
                new()
                {
                    Put = new Put
                    {
                        TableName = "Orders",
                        ConditionExpression = "attribute_not_exists(OrderId)",
                        Item = new Dictionary<string, AttributeValue>
                        {
                            { "CustomerId", new AttributeValue { S = customerId } },
                            { "OrderId", new AttributeValue { S = orderId } },
                            { "Quantity", new AttributeValue { N = qty.ToString() } },
                            { "TotalAmount", new AttributeValue { N = (qty * price).ToString("F2") } },
                            { "Status", new AttributeValue { S = "PAID" } }
                        }
                    }
                }
            }
        };

        try
        {
            await _client.TransactWriteItemsAsync(request);
        }
        catch (TransactionCanceledException ex)
        {
            // Обробка причин скасування транзакції
            var reasons = ex.CancellationReasons
                .Select((r, idx) => $"Елемент {idx}: {r.Code} (Опис: {r.Message})");
            
            throw new InvalidOperationException($"Транзакцію скасовано. Причини:\\n{string.Join("\\n", reasons)}", ex);
        }
    }
}

7. Обробка подій DynamoDB Streams у .NET Lambda

При активації DynamoDB Streams будь-яка зміна в таблиці записується у потік подій, який може викликати безсерверні функції AWS Lambda для побудови Event-Driven архітектури.

NuGet-пакети для Lambda-інтеграції

У проекті AWS Lambda серіалізація та обробка здійснюється за допомогою пакетів:

dotnet add package Amazon.Lambda.Core
dotnet add package Amazon.Lambda.Serialization.SystemTextJson
dotnet add package Amazon.Lambda.DynamoDBEvents

Приклад реалізації Lambda-обробника (Function.cs)

using Amazon.Lambda.Core;
using Amazon.Lambda.DynamoDBEvents;
using System.Text.Json;

// Налаштування глобального серіалізатора Lambda
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]

namespace LambdaHandler;

public class OrderStreamProcessor
{
    public async Task FunctionHandlerAsync(DynamoDBEvent dynamoEvent, ILambdaContext context)
    {
        context.Logger.LogInformation($"Отримано пакет подій. Кількість записів: {dynamoEvent.Records.Count}");

        foreach (var record in dynamoEvent.Records)
        {
            context.Logger.LogInformation($"EventID: {record.EventID} | Назва події: {record.EventName}");

            switch (record.EventName)
            {
                case "INSERT":
                    // Зчитування нових даних
                    var newItemMap = record.Dynamodb.NewImage;
                    string customerId = newItemMap["CustomerId"].S;
                    string orderId = newItemMap["OrderId"].S;
                    context.Logger.LogInformation($"[Створення] Замовлення {orderId} додано для клієнта {customerId}");
                    break;

                case "MODIFY":
                    // Порівняння змін (Old Image проти New Image)
                    var oldImage = record.Dynamodb.OldImage;
                    var newImage = record.Dynamodb.NewImage;
                    
                    if (oldImage.TryGetValue("Status", out var oldStatus) && 
                        newImage.TryGetValue("Status", out var newStatus))
                    {
                        context.Logger.LogInformation($"[Оновлення] Статус замовлення змінено: {oldStatus.S} -> {newStatus.S}");
                    }
                    break;

                case "REMOVE":
                    // Перевірка джерела видалення (Користувач чи TTL-сервіс)
                    bool isTtlDeletion = record.UserIdentity?.Type == "Service" && 
                                         record.UserIdentity?.PrincipalId == "dynamodb.amazonaws.com";

                    if (isTtlDeletion)
                    {
                        context.Logger.LogInformation($"[TTL] Запис автоматично видалено сервісом TTL: {record.Dynamodb.OldImage["OrderId"].S}");
                    }
                    else
                    {
                        context.Logger.LogInformation($"[Видалення] Користувач видалив запис: {record.Dynamodb.OldImage["OrderId"].S}");
                    }
                    break;
            }
        }

        await Task.CompletedTask;
    }
}

!IMPORTANT Клас AttributeValue у бібліотеці Amazon.Lambda.DynamoDBEvents — це інший тип порівняно з Amazon.DynamoDBv2.Model.AttributeValue у AWS SDK. Вони мають ідентичні за назвою властивості (S, N, BOOL), але розташовані в різних збірках. Якщо вам потрібно перетворити карту атрибутів Lambda-події в об'єкти домену, скористайтеся JSON-трансляцією або ручним перемальовуванням полів.


8. Практичні Best Practices для розробників .NET

Для забезпечення стабільної роботи .NET додатків під високим навантаженням дотримуйтеся наступних правил конфігурації клієнта:

  1. Керування TCP-підключеннями (Connection pooling): Реєструйте IAmazonDynamoDB виключно як Singleton. Під капотом SDK використовує HttpClient, який автоматично утилізує з'єднання. Налаштувати ліміти з'єднань можна через AmazonDynamoDBConfig:
    var config = new AmazonDynamoDBConfig
    {
        // Збільшуємо ліміт підключень (за замовчуванням 50) для систем із великим паралелізмом
        ConnectionLimit = 500
    };
    
  2. Обробка таймаутів та мережевих помилок: SDK за замовчуванням робить повторні спроби (Exponential Backoff) для помилок типу ProvisionedThroughputExceededException або тимчасових втрат з'єднання. Налаштувати кількість ретриїв можна вручну:
    var config = new AmazonDynamoDBConfig
    {
        MaxErrorRetry = 5 // Кількість автоматичних повторних спроб
    };
    
  3. Локалізація та парсинг чисел (CultureInfo): При ручному формуванні AttributeValue.N завжди передавайте числа через інваріантну культуру price.ToString(CultureInfo.InvariantCulture) or "F2", щоб запобігти генерації коми замість крапки (89,99 замість 89.99), що призведе до помилки десеріалізації з боку AWS API.

Практичний приклад: Інтерактивна робота з DynamoDB через AWS CLI

Перш ніж переходити до розробки коду на C#, важливо зрозуміти низькорівневу механіку DynamoDB «на дотик». Найкращий спосіб зробити це — скористатися утилітою командного рядка AWS CLI для взаємодії з вашим хмарним акаунтом AWS. Це дозволить наочно побачити структуру JSON-документів з явним зазначенням типів даних (S, N, BOOL), які надсилаються в HTTP-запитах до DynamoDB API.

Налаштування середовища та автентифікації

Для виконання запитів до хмари AWS переконайтеся, що на вашій машині встановлено AWS CLI та налаштовано профіль автентифікації за замовчуванням (Default Profile) або інший профіль з відповідними правами доступу до DynamoDB (наприклад, політика AmazonDynamoDBFullAccess):

# Конфігурація доступу до реального акаунту AWS
aws configure

Утиліта попросить ввести:

  • AWS Access Key ID: ваш ідентифікатор ключа доступу IAM.
  • AWS Secret Access Key: ваш секретний ключ доступу IAM.
  • Default region name: регіон вашої бази (наприклад, eu-central-1).
  • Default output format: формат виводу, наприклад json.

Після цього всі команди будуть за замовчуванням виконуватися у вашому хмарному середовищі AWS.

!TIPАльтернатива (Локальний запуск): Якщо ви хочете запустити ці ж команди локально на Docker-контейнері DynamoDB Local (з Кроку 1 альтернативного шляху C#-прикладу), додавайте до кожної команди параметр --endpoint-url http://localhost:8000 та вказуйте будь-який фейковий профіль за допомогою --profile local.


Крок 1: Створення таблиці (Create Table)

Створимо таблицю SandboxTasks із композитним ключем (Partition Key UserId типу String та Sort Key TaskId типу String), а також додамо локальний вторинний індекс (LSI) для фільтрації за пріоритетом.

aws dynamodb create-table \
    --table-name SandboxTasks \
    --attribute-definitions \
        AttributeName=UserId,AttributeType=S \
        AttributeName=TaskId,AttributeType=S \
        AttributeName=Priority,AttributeType=S \
    --key-schema \
        AttributeName=UserId,KeyType=HASH \
        AttributeName=TaskId,KeyType=RANGE \
    --local-secondary-indexes \
        "[
            {
                \"IndexName\": \"Priority-index\",
                \"KeySchema\": [
                    {\"AttributeName\": \"UserId\", \"KeyType\": \"HASH\"},
                    {\"AttributeName\": \"Priority\", \"KeyType\": \"RANGE\"}
                ],
                \"Projection\": {
                    \"ProjectionType\": \"ALL\"
                }
            }
        ]" \
    --billing-mode PAY_PER_REQUEST

Що тут відбувається:

  • AttributeDefinitions: описує схему типів даних лише для полів, які використовуються в ключах (UserId, TaskId, Priority).
  • KeySchema: визначає роль полів у первинному ключі.
  • LocalSecondaryIndexes: створює індекс Priority-index, який дозволяє сортувати та шукати задачі конкретного користувача за пріоритетом.

Крок 2: Додавання запису (PutItem)

Запишемо перше завдання до бази. Оскільки DynamoDB є безсхемною (schemaless) базою даних, ми можемо передавати будь-яку кількість додаткових атрибутів (наприклад, Title, Done, StepsCount), не оголошуючи їх у схемі таблиці:

aws dynamodb put-item \
    --table-name SandboxTasks \
    --item '{
        "UserId": {"S": "user-42"},
        "TaskId": {"S": "task-001"},
        "Title": {"S": "Написати документацію по DynamoDB"},
        "Priority": {"S": "HIGH"},
        "Done": {"BOOL": false},
        "StepsCount": {"N": "5"},
        "Tags": {"L": [{"S": "aws"}, {"S": "nosql"}, {"S": "cli"}]}
    }'

Зверніть увагу:

  • Кожне значення загорнуте в об'єкт із ключем типу даних: {"S": "..."} для String, {"BOOL": ...} для Boolean, {"N": "..."} для Number (числа передаються як рядки для запобігання втрати точності), {"L": [...]} для List.

Крок 3: Читання запису за ключем (GetItem)

Для точкового зчитування запису (Point Read) ми повинні передати повний первинний ключ (як Partition Key, так і Sort Key):

aws dynamodb get-item \
    --table-name SandboxTasks \
    --key '{
        "UserId": {"S": "user-42"},
        "TaskId": {"S": "task-001"}
    }'

Очікувана відповідь від бази:

{
    "Item": {
        "StepsCount": {"N": "5"},
        "UserId": {"S": "user-42"},
        "TaskId": {"S": "task-001"},
        "Done": {"BOOL": false},
        "Title": {"S": "Написати документацію по DynamoDB"},
        "Priority": {"S": "HIGH"},
        "Tags": {"L": [{"S": "aws"}, {"S": "nosql"}, {"S": "cli"}]}
    }
}

Крок 4: Умовне оновлення з перевіркою версії (UpdateItem & ConditionExpression)

Виконаємо оновлення статусу виконання задачі (Done = true), але додамо умову: оновлювати лише якщо задача має статус Done = false (Condition Expression). Це демонструє базову механіку уникнення конфліктів:

aws dynamodb update-item \
    --table-name SandboxTasks \
    --key '{
        "UserId": {"S": "user-42"},
        "TaskId": {"S": "task-001"}
    }' \
    --update-expression "SET Done = :new_status" \
    --condition-expression "Done = :expected_status" \
    --expression-attribute-values '{
        ":new_status": {"BOOL": true},
        ":expected_status": {"BOOL": false}
    }' \
    --return-values ALL_NEW

Що тут відбувається:

  • UpdateExpression: визначає зміну атрибута за допомогою плейсхолдера :new_status.
  • ConditionExpression: вказує, що операція запису відбудеться лише якщо поточне значення Done дорівнює плейсхолдеру :expected_status (тобто false).
  • ReturnValues ALL_NEW: каже базі повернути оновлений об'єкт.

Результат повторного виконання: Якщо ви виконаєте цю саму команду вдруге, операція завершиться помилкою: An error occurred (ConditionalCheckFailedException) when calling the UpdateItem operation: Оскільки статус у базі вже став true, умова Done = false більше не виконується.


Крок 5: Пошук за допомогою запитів (Query)

А. Запит за головним ключем

Отримаємо список усіх задач користувача user-42, які починаються на префікс task-:

aws dynamodb query \
    --table-name SandboxTasks \
    --key-condition-expression "UserId = :uid AND TaskId begins_with(:task_prefix)" \
    --expression-attribute-values '{
        ":uid": {"S": "user-42"},
        ":task_prefix": {"S": "task-"}
    }'

Б. Запит за допомогою вторинного індексу LSI

Тепер скористаємося індексом Priority-index, щоб отримати лише задачі з високим пріоритетом (HIGH), відсортовані за спаданням:

aws dynamodb query \
    --table-name SandboxTasks \
    --index-name Priority-index \
    --key-condition-expression "UserId = :uid AND Priority = :priority" \
    --expression-attribute-values '{
        ":uid": {"S": "user-42"},
        ":priority": {"S": "HIGH"}
    }'

Крок 6: Видалення запису та очищення (DeleteItem)

Видалимо створену задачу:

aws dynamodb delete-item \
    --table-name SandboxTasks \
    --key '{
        "UserId": {"S": "user-42"},
        "TaskId": {"S": "task-001"}
    }'

Для повного видалення таблиці після експериментів виконайте:

aws dynamodb delete-table \
    --table-name SandboxTasks

Практичний приклад: C# Web API + DynamoDB від А до Я

У цьому розділі ми розберемо реалізацію повноцінного production-готового веб-сервісу з управління завданнями (Task Management API) на ASP.NET Core від А до Я. Наш застосунок підтримуватиме повний CRUD-цикл за допомогою OPM ORM (DynamoDBContext) та вибірку за глобальним вторинним індексом (GSI).

Основним середовищем розгортання є реальна хмара AWS (з керуванням ресурсами через веб-консоль та автентифікацією через AWS CLI), а використання локального контейнера з DynamoDB Local охарактеризовано як альтернативний спрощений шлях.


Крок 1: Підготовка інфраструктури (AWS Console, Credentials & Альтернатива)

Для початку роботи нам необхідно створити таблицю в хмарі AWS та налаштувати права доступу на локальній машині розробника.

1. Створення таблиці через AWS Management Console

Для створення таблиці вручну виконайте такі кроки:

  1. Відкрийте AWS Management Console та авторизуйтесь у вашому акаунті.
  2. У полі пошуку сервісів введіть DynamoDB та перейдіть на головну сторінку сервісу.
  3. Натисніть кнопку Create table (Створити таблицю).
  4. У секції Table details вкажіть такі параметри:
    • Table name: UserTasks
    • Partition key: UserId (тип залишіть як String)
    • Sort key: TaskId (тип залишіть як String)
  5. У секції Table settings виберіть пункт Customize settings (Налаштувати параметри):
    • Capacity calculator: пропустіть цей пункт.
    • Table class: виберіть DynamoDB Standard (за замовчуванням).
    • Read/write capacity mode: виберіть On-demand (за запитом). Це найкращий варіант для навчання та тестування, оскільки ви платите лише за фактично виконані запити, а не за зарезервовану ємність (WCU/RCU).
  6. Прокрутіть вниз до секції Global secondary indexes (Глобальні вторинні індекси) та натисніть Create index:
    • Partition key: Status (тип String)
    • Index name: автоматично підставиться Status-index (якщо ні, введіть вручну).
    • Attribute projections: виберіть All (це дозволить копіювати всі поля запису у вторинний індекс).
    • Натисніть Create index у спливаючому вікні.
  7. Прокрутіть сторінку до самого кінця та натисніть кнопку Create table. Створення таблиці триває близько 10-30 секунд, статус зміниться на Active.

2. Налаштування локальних AWS Credentials

Веб-додаток на C# під час запуску локально на вашому комп'ютері використовує офіційний AWS SDK. Щоб він міг авторизуватися в хмарі AWS, потрібно налаштувати облікові дані на локальній машині за допомогою AWS CLI:

  1. Відкрийте термінал та запустіть команду налаштування:
    aws configure
    
  2. Утиліта запитає у вас конфігураційні дані:
    • AWS Access Key ID: введіть ваш ключ доступу IAM-коривувача (наприклад, AKIAIOSFODNN7EXAMPLE).
    • AWS Secret Access Key: введіть ваш секретний ключ (наприклад, wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY).
    • Default region name: введіть регіон, у якому ви створили таблицю (наприклад, eu-central-1).
    • Default output format: залишіть порожнім або введіть json.

!IMPORTANT Переконайтеся, що ваш IAM-користувач, ключі якого ви ввели, має права доступу на роботу з DynamoDB (наприклад, політику AmazonDynamoDBFullAccess або точкові права на читання/запис до таблиці UserTasks).

Тепер під час запуску додатку AWS SDK автоматично підхопить конфігурацію з файлу ~/.aws/credentials та надішле запити до реальної бази у хмарі.

3. Альтернатива: Локальний запуск через Docker Compose (DynamoDB Local)

Якщо ви бажаєте протестувати код локально без використання реального хмарного акаунту AWS, ви можете запустити базу даних у локальному Docker-контейнері:

  1. Створіть у корені проєкту файл docker-compose.yml:
    docker-compose.yml
    version: '3.8'
    
    services:
      dynamodb-local:
        image: amazon/dynamodb-local:latest
        container_name: dynamodb-local
        ports:
          - "8000:8000"
        volumes:
          - "./docker/dynamodb:/home/dynamodblocal-data"
        command: "-jar DynamoDBLocal.jar -sharedDb -dbPath /home/dynamodblocal-data"
    
      dynamodb-admin:
        image: aaronshaf/dynamodb-admin:latest
        container_name: dynamodb-admin
        ports:
          - "8001:8001"
        environment:
          - DYNAMO_ENDPOINT=http://dynamodb-local:8000
        depends_on:
          - dynamodb-local
    
  2. Запустіть Docker-контейнер командою:
    docker compose up -d
    
  3. Після старту перейдіть за адресою http://localhost:8001, щоб користуватися графічним інтерфейсом адміністрування бази. При виборі локального варіанту обов'язково встановіть "LocalMode": true у файлі конфігурації appsettings.json (описано у Кроці 3), і додаток автоматично створить потрібну таблицю та індекс при запуску.

Крок 2: Створення та структура C# проєкту (ASP.NET Core Web API)

Створимо новий проєкт веб-інтерфейсу за допомогою .NET CLI та додамо модель даних для завдань. Кожне завдання матиме:

  • Partition Key: UserId (ідентифікатор користувача)
  • Sort Key: TaskId (ідентифікатор завдання)
  • Атрибут Status (наприклад, TODO, IN_PROGRESS, DONE)
  • Атрибут Priority
  • Мапування GSI Status-index для вибірки завдань за статусом.

1. Ініціалізація проєкту та встановлення NuGet-пакетів

Виконайте наступні команди у терміналі:

# Створюємо Minimal Web API проєкт
dotnet new web -o TaskManager.Api

# Переходимо до папки проєкту
cd TaskManager.Api

# Встановлюємо пакети AWS SDK
dotnet add package AWSSDK.DynamoDBv2
dotnet add package AWSSDK.Extensions.NETCore.Setup

2. Створення моделі сутності TaskItem

Створимо клас TaskItem, розмітивши його атрибутами OPM ORM:


Крок 3: Налаштування клієнта, Dependency Injection та автоматична ініціалізація бази

Оскільки в локальному режимі таблиця UserTasks та глобальний індекс Status-index не існують за замовчуванням, ми налаштуємо спеціальний Database Initializer, який перевірить наявність таблиці під час старту веб-сервісу і створить її у разі відсутності.

1. Налаштування файлу конфігурації

Створіть або оновіть файл appsettings.json, додавши секцію "DynamoDb" з конфігурацією локального підключення:

2. Реалізація DynamoDbInitializer

Створимо інфраструктурний клас, який за допомогою низькорівневого API (IAmazonDynamoDB) автоматично конструює схему таблиці:

3. Реєстрація DI та запуск ініціалізатора при старті

Оновимо файл Program.cs, зареєструвавши клієнт SDK (з урахуванням режиму Local/AWS), ORM контекст та репозиторії:


Крок 4: Реалізація CRUD репозиторію

Створимо рівень доступу до даних (Data Access Layer), який приховує деталі взаємодії з DynamoDBContext за допомогою чистого інтерфейсу.

1. Створення інтерфейсу репозиторію

Створимо інтерфейс ITaskRepository для основних операцій над завданнями:

2. Реалізація репозиторію за допомогою OPM ORM

Створимо клас DynamoDBTaskRepository. Він використовуватиме IDynamoDBContext для CRUD, а також локальне перевизначення конфігурації для пошуку за індексом GSI:

Крок 5: Додавання Minimal API ендпоінтів та верифікація роботи

На завершальному кроці ми додамо REST-ендпоінти для взаємодії з завданнями та протестуємо всі сценарії, включно з валідацією оптимістичного блокування при паралельному оновленні.

1. Додавання ендпоінтів у Program.cs

Оновіть файл Program.cs, додавши групу ендпоінтів для роботи з завданнями. Зверніть увагу на обробку помилок при оновленні записів з невірною версією (Optimistic Locking):

2. Верифікація роботи через HTTP-запити (cURL)

Для тестування розробленого API запустіть проєкт за допомогою команди dotnet run. Протестуємо основні сценарії за допомогою curl.

А. Створення нового завдання

Відправте POST-запит для створення завдання для користувача user-100:

curl -X POST http://localhost:5000/api/tasks \
  -H "Content-Type: application/json" \
  -d '{
    "UserId": "user-100",
    "Title": "Вивчити AWS DynamoDB SDK",
    "Description": "Розібрати Low-Level, Document та OPM інтерфейси",
    "Status": "IN_PROGRESS",
    "Priority": "HIGH"
  }'

Очікувана відповідь (201 Created):

{
  "userId": "user-100",
  "taskId": "7f8b9c2d6e3f4a5b8c9d0e1f2a3b4c5d",
  "title": "Вивчити AWS DynamoDB SDK",
  "description": "Розібрати Low-Level, Document та OPM інтерфейси",
  "status": "IN_PROGRESS",
  "priority": "HIGH",
  "createdAt": "2026-06-04T12:00:00Z",
  "version": 1
}

Примітка: скопіюйте згенерований taskId для наступних запитів.

Б. Отримання завдань користувача (Query за Hash-ключем)

curl -X GET http://localhost:5000/api/tasks/user-100

Очікувана відповідь (200 OK):

[
  {
    "userId": "user-100",
    "taskId": "7f8b9c2d6e3f4a5b8c9d0e1f2a3b4c5d",
    "title": "Вивчити AWS DynamoDB SDK",
    "status": "IN_PROGRESS",
    "version": 1
  }
]

В. Вибірка за статусом (Query через GSI "Status-index")

curl -X GET http://localhost:5000/api/tasks/status/IN_PROGRESS

Поверне всі завдання з бази, у яких поле Status дорівнює IN_PROGRESS (навіть якщо вони належать різним користувачам).

Г. Оновлення та тест Optimistic Locking (Конфлікт версій)

Спробуємо оновити статус завдання на DONE. Спочатку відправимо коректний запит з version: 1:

curl -X PUT http://localhost:5000/api/tasks \
  -H "Content-Type: application/json" \
  -d '{
    "UserId": "user-100",
    "TaskId": "7f8b9c2d6e3f4a5b8c9d0e1f2a3b4c5d",
    "Title": "Вивчити AWS DynamoDB SDK",
    "Description": "Розібрати Low-Level, Document та OPM інтерфейси",
    "Status": "DONE",
    "Priority": "HIGH",
    "Version": 1
  }'

Очікувана відповідь (200 OK):

{
  "userId": "user-100",
  "taskId": "7f8b9c2d6e3f4a5b8c9d0e1f2a3b4c5d",
  "status": "DONE",
  "version": 2
}

Зверніть увагу: поле version автоматично збільшилось на 2.

Тепер змоделюємо конфлікт паралельного доступу. Спробуємо знову виконати оновлення, але передамо застарілу версію version: 1 (хоча у базі вже збережена версія 2):

curl -X PUT http://localhost:5000/api/tasks \
  -H "Content-Type: application/json" \
  -d '{
    "UserId": "user-100",
    "TaskId": "7f8b9c2d6e3f4a5b8c9d0e1f2a3b4c5d",
    "Title": "Вивчити AWS DynamoDB SDK",
    "Status": "DONE",
    "Version": 1
  }'

Очікувана відповідь (409 Conflict):

{
  "message": "Конфлікт паралельного оновлення. Версія запису у базі була змінена іншим процесом."
}

Це доводить, що механізм оптимістичного блокування успішно спрацював, і база відхилила перезапис застарілими даними!

Д. Видалення завдання

curl -X DELETE http://localhost:5000/api/tasks/user-100/7f8b9c2d6e3f4a5b8c9d0e1f2a3b4c5d

Очікувана відповідь (204 No Content).

Підсумок модуля

У цьому навчальному модулі ми детально розглянули архітектурні засади, правила проектування та програмну реалізацію систем на базі Amazon DynamoDB:

Частина 1 — Основи: Досліджено реляційну та NoSQL парадигми, типи первинних ключів (Simple HASH проти Composite HASH+RANGE), базові CRUD операції на рівні запитів, розрахунок RCU/WCU для Strongly та Eventually consistent читання та запису.

Частина 2 — Вторинні індекси: Порівняно архітектурні особливості LSI (Local Secondary Index) та GSI (Global Secondary Index), розібрано патерн Sparse Index для оптимізації витрат та Single-Table design.

Частина 3 — Режими керування ємністю: Вивчено специфіку роботи режимів Provisioned (з автоматичним масштабуванням CloudWatch Alarms) та On-Demand (PAY_PER_REQUEST), механізми Burst Capacity та експоненціальної затримки Full Jitter.

Частина 4 — Streams та Транзакції: Деталізовано CDC-рішення DynamoDB Streams з його Shards та інтеграцією через ESM з AWS Lambda, а також реалізацію ACID транзакцій за протоколом 2PC (Two-Phase Commit).

Частина 5 — TTL, Global Tables та Best Practices: Досліджено механізм автоматичного безкоштовного очищення застарілих даних за допомогою TTL, налаштування геореплікації Global Tables (Active-Active) та стратегії уникнення гарячих партицій.

Частина 6 — Інтеграція з .NET: Розглянуто архітектурні рівні інтеграції C# клієнтів (Low-Level, Document, Object Persistence Model), розробку CRUD репозиторіїв та обробників Streams подій у Lambda.

Наступний архітектурний крок: Для високонавантажених читанням систем (Read-Heavy workloads) розгляньте впровадження DynamoDB Accelerator (DAX) — повністю сумісного in-memory кеш-сервісу, який знижує час відгуку до мікросекундного рівня без необхідності зміни бізнес-коду додатка.
Copyright © 2026