DynamoDB Indexes (GSI & LSI)
DynamoDB is fast only when you query by a table’s key. But real apps need to look up data by other fields too — find all orders for a customer, all posts by date, all users with a given status. A secondary index is a separate, automatically-maintained copy of your table organized around a different key, so you can query those other fields efficiently. DynamoDB gives you two kinds: a Global Secondary Index (GSI) and a Local Secondary Index (LSI). Picking the right one — and projecting the right attributes — is the difference between cheap, fast queries and expensive table scans.
Why you need an index
In DynamoDB your table has a partition key (PK) (the value that decides which physical partition stores the item) and optionally a sort key (SK) (which orders items inside that partition). You can only run an efficient Query against that key. To query by anything else you must either Scan the whole table (slow and expensive) or create an index.
A secondary index stores the same items under a different key layout. DynamoDB keeps it in sync for you on every write. You then Query the index instead of the table.
GSI vs LSI — when to use which
| Feature | Global Secondary Index (GSI) | Local Secondary Index (LSI) |
|---|---|---|
| Partition key | Any attribute (different from table) | Same as the table’s partition key |
| Sort key | Any attribute (optional) | An alternate attribute |
| When you can create it | Any time, even after launch | Only at table creation |
| Capacity / throughput | Its own read/write capacity | Shares the table’s capacity |
| Consistency | Eventually consistent only | Supports strongly consistent reads |
| Item size limit | No extra limit | All items per PK + indexes must fit in 10 GB |
| How many | Up to 20 per table | Up to 5 per table |
When to use a GSI: you want to query by a completely different attribute than the table’s partition key — for example, look up a user by email when the table is keyed by userId. This is the common case.
When to use an LSI: you want the same partition key but a different sort order — for example, items keyed by customerId that you usually sort by orderDate, but sometimes need sorted by totalAmount. Use an LSI only when you also need strongly consistent reads, since GSIs cannot give those.
Important: an LSI must be defined when you create the table and can never be added, changed, or removed afterward. A GSI can be added or deleted at any time. If you are unsure, prefer a GSI — it is far more flexible.
Creating a GSI
A GSI is defined by its own key schema and (for provisioned tables) its own throughput. Decide which attributes to project (copy) into the index. Choices are KEYS_ONLY, INCLUDE (specific attributes), or ALL.
Console steps
- Open the DynamoDB console and choose your table (e.g.
Orders). - Go to the Indexes tab and click Create index.
- Set the Partition key for the index (e.g.
customerId, type String) and an optional Sort key (e.g.orderDate). - Enter an Index name (e.g.
customerId-orderDate-index). - Choose Attribute projections: All, Keys only, or Include a chosen list.
- If your table uses Provisioned capacity, set the index’s read/write capacity. On On-demand it scales automatically.
- Click Create index and wait for status Active (large tables backfill in the background).
AWS CLI
aws dynamodb update-table \
--table-name Orders \
--attribute-definitions \
AttributeName=customerId,AttributeType=S \
AttributeName=orderDate,AttributeType=S \
--global-secondary-index-updates '[{
"Create": {
"IndexName": "customerId-orderDate-index",
"KeySchema": [
{"AttributeName": "customerId", "KeyType": "HASH"},
{"AttributeName": "orderDate", "KeyType": "RANGE"}
],
"Projection": {"ProjectionType": "ALL"}
}
}]'
Output:
{
"TableDescription": {
"TableName": "Orders",
"TableStatus": "UPDATING",
"GlobalSecondaryIndexes": [
{
"IndexName": "customerId-orderDate-index",
"IndexStatus": "CREATING",
"Backfilling": true
}
]
}
}
Creating an LSI (at table creation only)
Because an LSI shares the table’s partition key, you declare it inside create-table:
aws dynamodb create-table \
--table-name Orders \
--attribute-definitions \
AttributeName=customerId,AttributeType=S \
AttributeName=orderId,AttributeType=S \
AttributeName=totalAmount,AttributeType=N \
--key-schema \
AttributeName=customerId,KeyType=HASH \
AttributeName=orderId,KeyType=RANGE \
--local-secondary-indexes '[{
"IndexName": "customerId-totalAmount-index",
"KeySchema": [
{"AttributeName": "customerId", "KeyType": "HASH"},
{"AttributeName": "totalAmount", "KeyType": "RANGE"}
],
"Projection": {"ProjectionType": "KEYS_ONLY"}
}]' \
--billing-mode PAY_PER_REQUEST
Querying an index
Querying an index looks just like querying a table — you add --index-name. Here we find every order for a customer placed in 2026, newest first:
aws dynamodb query \
--table-name Orders \
--index-name customerId-orderDate-index \
--key-condition-expression "customerId = :c AND begins_with(orderDate, :y)" \
--expression-attribute-values '{":c": {"S": "CUST-1024"}, ":y": {"S": "2026"}}' \
--no-scan-index-forward
Output:
{
"Items": [
{"orderId": {"S": "ord-0a1b2c3d"}, "customerId": {"S": "CUST-1024"},
"orderDate": {"S": "2026-06-14"}, "totalAmount": {"N": "89.50"}}
],
"Count": 1,
"ScannedCount": 1
}
The projection gotcha
The biggest cost trap is projecting too few attributes. If your query needs a field that the index does not project, DynamoDB silently performs an extra read back to the base table for each item to fetch it. That doubles read cost and adds latency.
Cost note:
ProjectionType: ALLmakes queries fast but stores a full copy of every item in the index (you pay for that storage and write capacity twice).KEYS_ONLYis cheapest to store but forces base-table lookups for any extra field. Project exactly the attributes your queries return — no more, no less.
Also remember a GSI has its own throughput. On a provisioned table, if the GSI’s write capacity is too low, writes to the index can be throttled — and that throttling can back-pressure writes to the whole table. Watch the ThrottledRequests metric per index in CloudWatch.
Best practices
- Write down your access patterns (the exact queries your app runs) before designing keys and indexes; model the indexes to match that list.
- Prefer a GSI unless you specifically need strongly consistent reads on the same partition key — then use an LSI, and define it at table creation.
- Project only the attributes your queries read; avoid
ALLunless you truly need the full item, and never rely onKEYS_ONLYif you fetch extra fields. - On provisioned tables, give each GSI enough write capacity, or use On-demand billing so the index scales with the table.
- Watch per-index
ConsumedWriteCapacityUnitsandThrottledRequestsin CloudWatch; an under-provisioned GSI can throttle base-table writes. - Remember GSI queries are eventually consistent — a freshly written item may not appear in the index for a fraction of a second.