Intermediate7 min read

Sorting DynamoDB on a Changing Attribute

You model a sort key around an attribute so you can query items in its order — then the attribute changes. A ticket's status, an order's state, a task's priority. DynamoDB's rule: you cannot update a key attribute in place. A primary key is immutable for the life of the item. Change a value that's part of the key and you're not editing an item — you're moving it, which DynamoDB makes you do explicitly.

Can you change a DynamoDB sort key?

No. A sort key is part of the primary key, and DynamoDB key attributes are immutable — UpdateItem cannot edit a partition or sort key value, and there is no "move item" operation. To change it, delete the old item and put a new one, or keep the volatile value on a GSI sort key instead.

  • Key attributes are immutable. You can't UpdateItem a partition or sort key value — DynamoDB has no "move item" operation.
  • To change a key value you delete the old item and put a new one — ideally in a transaction so it's atomic.
  • Better: keep the volatile value off the base-table key and put it on a GSI sort key instead — GSI keys can change, because updating the base item just re-propagates the index entry.
  • Choose sort keys that don't change (timestamps, immutable ids) whenever the access pattern allows.

The problem: a status you want to sort by, that keeps changing

Say you run a support desk and want to list a team's tickets ordered by status, so you put status in the sort key:

PK: TEAM#7   SK: STATUS#open#TICKET#8842

Now the ticket moves to pending. You'd like to just UpdateItem the sort key to STATUS#pending#TICKET#8842 — but DynamoDB rejects any write that changes a key attribute. The key is the item's address; you can't edit the address in place. The status you chose to sort by is exactly the thing that won't sit still.

Option 1: delete and recreate (atomically)

If the value must live in the base-table key, changing it means removing the old item and writing the new one:

1. DeleteItem  PK=TEAM#7  SK=STATUS#open#TICKET#8842
2. PutItem     PK=TEAM#7  SK=STATUS#pending#TICKET#8842  (same attributes)

Do it inside a TransactWriteItems so the delete and the put either both succeed or both fail — otherwise a crash between them loses the ticket or duplicates it. This works, but every status change is now two writes plus a transaction; fine for occasional changes, costly for hot ones.

Option 2: keep the mutable value off the base key (preferred)

Make the base-table key something immutable (the ticket id) and put the volatile, sortable value on a GSI sort key.

Base:  PK: TICKET#8842   status: "open"   teamId: TEAM#7
GSI:   GSI1PK: TEAM#7    GSI1SK: STATUS#open#TICKET#8842

Now changing status is a plain UpdateItem on the base item's status attribute — which DynamoDB allows, because status isn't a base-table key. DynamoDB then re-propagates the GSI entry automatically to its new sorted position. One API call, with atomicity handled for you — no transaction, no delete dance (under the hood DynamoDB still deletes the old index entry and writes the new one, so an indexed change costs ~3 write units against the ~4 of a transactional delete-and-put).

YesNo, it's a GSI sort keyStatus changes open to pendingIs the value in a base-table key?Delete + recreate in a transactionPlain UpdateItem; GSIre-propagates

The GSI is eventually consistent and costs extra storage/writes — but for a value that changes often, that's somewhat cheaper (~3 vs 4 write units) and much simpler than delete-and-recreate on every change.

Designing the keys in DynoTable

Build and preview the key conditions for both the base read and the GSI read in the DynamoDB expression builder.

In DynoTable, you then pick which index a query runs through and watch the volatile value sort on the GSI while the base item keeps its immutable key — both reads side by side on real data.

Querying a status-sorted GSI while the base item keeps an immutable key in DynoTable.
Querying a status-sorted GSI while the base item keeps an immutable key in DynoTable.

Pitfalls + next steps

  • Never try to UpdateItem a key attribute — it's rejected; key values are fixed for the item's life.
  • If you must move it, do delete+put in a transaction — never as two unguarded writes.
  • Prefer immutable base keys + a GSI for any attribute you sort by and mutate.
  • Don't forget GSI eventual consistency — the re-sorted entry appears after a brief propagation delay.
  • Related: sort-key strategies, GSI vs LSI, transactions.

Want to see how a mutable attribute sorts on a GSI versus the base table? Download DynoTable and explore your indexes directly.

Write cost: delete-and-put vs GSI update

Rough WCU comparison for a 1 KB ticket item in us-east-1 on-demand (actual billing follows AWS rounding rules):

PatternAPI callsTypical WCU impact
Transactional delete + put on base keyTransactWriteItems (2 ops)~2× item size per op in transaction pricing
Update status attribute; GSI re-propagatesOne UpdateItemBase write + GSI write (~2 WCU for 1 KB item + projected attrs)

The GSI path avoids application-level orchestration and eliminates the window where a crash between delete and put loses the row. You trade eventual consistency on the index read for simpler writes.

Model your item size and update rate in the pricing calculator when status changes fire many times per minute.

Sparse GSI for status-sorted lists

If only open tickets need a status-sorted queue, use a sparse index: write GSI1PK = TEAM#7 and GSI1SK = STATUS#open#... only while status = open. When the ticket closes, remove or omit the GSI key attributes on update — the item drops out of the index without a delete-and-put on the base sort key.

That keeps the index small and avoids indexing closed tickets you never list.

Immutable base keys to prefer

Volatile fieldBase-table SKBetter base SKVolatile field lives on
Order statusSTATUS#shipped#ORD#99ORD#99GSI sort or attribute
Task priorityP#1#TASK#12TASK#12GSI sort
User display nameNAME#alice#USER#5USER#5Non-key attribute

Timestamps and immutable ids (CREATED#2026-06-27T10:00:00Z, TICKET#8842) make stable base sort keys when you need chronological order on the base table itself.

Design the GSI before coding

Map access patterns in the single-table design tool — enter "list open tickets by team, priority order" and inspect the suggested GSI1PK / GSI1SK templates. Then build the key condition in the expression builder and emit a paginated query with the query builder for integration tests.

Read-your-writes after status change

After UpdateItem, a strongly consistent read on the base table shows the new status immediately. A query on the GSI may lag briefly. UI flows that redirect to a GSI-sorted queue should tolerate stale rows or re-fetch from the base table by id when precision matters.

Updated