GSI migration wizard
Add a global secondary index to a live table: backfill a computed key with dry-run, idempotency, rollback and generated code.
The GSI migration wizard adds a global secondary index to a live, populated table — including computing and backfilling a brand-new key attribute onto existing items first — with a dry-run preview, idempotent writes, a rollback, and generated code to keep new writes consistent.
The problem it solves
Creating a GSI on a key attribute your items already have is easy: DynamoDB indexes it for you. The hard case is a new access pattern — you want to query by an attribute that doesn’t exist as a key yet, often a composite like STATUS#${status}. Those items need the key written before the index can find them. The wizard handles both cases, and crucially, in the safe order.
Safe ordering
The wizard always backfills the computed attribute onto every item first, and only then creates the index. DynamoDB backfills the index itself, completely, with no partially-populated window:
1. Compute + write the key attribute on existing items (backfill)
2. Create the GSI (DynamoDB fills the index)
A plain “index an existing attribute” plan skips step 1 entirely — there’s nothing to write, so the wizard just creates the index.
Defining the index
Open the wizard from a Scan’s suggest GSI button, the partition-health distribute action, or directly. You configure:
- Index name and an optional entity scope (limit the backfill to one entity type — useful for overloaded GSIs that differ per entity).
- Partition key, and an optional sort key, each as either:
- Existing — index an attribute as-is, no write, or
- Template — compute the attribute per item, e.g.
STATUS#${status}(supports nested refs like${address.city}).
- Type (
SorN) and projection (ALLorKEYS_ONLY). - On a provisioned table, RCU/WCU for the new index.
The plan is validated continuously. The wizard blocks you with friendly messages on footguns DynamoDB would reject or that break idempotency: empty name, duplicate index name, missing attribute or template, the partition and sort key sharing a name, and a self-reference — a template that reads a to-be-written computed attribute, which would never converge.
Dry-run preview
Click preview to dry-run the plan over your loaded sample. It classifies every item without writing anything:
| Outcome | Meaning |
|---|---|
| Will write | The computed key differs and the item needs an update |
| Already ok | The item already carries the exact computed value (idempotent) |
| Missing source | A referenced attribute is absent — item is left out of the index |
| Invalid | The computed key is empty, or non-numeric for an N key — DynamoDB would reject it |
| Out of scope | The item isn’t the chosen entity type |
A few sample transformations are shown (id → attr=value), and a warning appears if some items can’t get the key. This is your chance to catch a wrong template before touching the table.
Executing
Execute runs a pre-flight against the live table — it must be ACTIVE, under the 20-GSI limit, and the index name must be free — then asks for a typed confirmation. The backfill then scans the whole table in pages of 500, writes only the items that need it via batch writes, and reports running counts (written / ok / missing / scanned). You can abort the backfill mid-run. After the backfill, the index is created.
Writes are idempotent: an item already matching is skipped, and comparison is type-aware (an N key compares numerically, so 8 and "8" don’t loop). Re-running a plan is safe.
Done: poll, rollback, generated code
On completion the wizard polls the new index until it’s ACTIVE and finished backfilling (DynamoDB populates it asynchronously), showing populating… until then. You also get:
- Rollback — delete the just-created GSI in one click if something looks wrong.
- Generated snippet — for template plans, an app-side
withGsiKey(item)function so your new writes keep populating the key going forward:
function withGsiKey(item) {
item["gsi1pk"] = `STATUS#${item.status}`;
return item;
}
Limitations to keep in mind
- Backfill is client-driven. Tablyne scans and batch-writes from your machine — it’s not a server-side job. A huge table means a long-running, capacity-consuming backfill; keep the wizard open until it finishes.
- The preview is sample-based; the execution scans the full table, so the real counts can exceed the preview’s.
- Items with missing/invalid source values are written without the key and won’t appear in the index — the preview tells you how many.
- Capacity cost: the backfill consumes write units and the scan consumes reads — estimate with the capacity calculator.