DynamoDB Indexes
Complete guide to Global Secondary Indexes (GSI) and Local Secondary Indexes (LSI)
DynamoDB indexes enable efficient queries on attributes other than the primary key. Understanding when and how to use indexes is crucial for application performance.
Index Types Overview
GSI vs LSI
- Global Secondary Index (GSI): Different partition key, queries across all partitions
- Local Secondary Index (LSI): Same partition key, different sort key
| Feature | GSI | LSI |
|---|---|---|
| Partition Key | Different from table | Same as table |
| Sort Key | Optional | Required |
| Creation | Anytime | Table creation only |
| Capacity | Separate from table | Shares with table |
| Consistency | Eventually consistent | Eventual or Strong |
| Size | Unlimited | 10 GB per partition |
Global Secondary Indexes (GSI)
Creating a GSI
aws dynamodb create-table \
--table-name Orders \
--attribute-definitions \
AttributeName=orderId,AttributeType=S \
AttributeName=customerId,AttributeType=S \
AttributeName=orderDate,AttributeType=S \
--key-schema \
AttributeName=orderId,KeyType=HASH \
--global-secondary-indexes '[
{
"IndexName": "CustomerIndex",
"KeySchema": [
{"AttributeName": "customerId", "KeyType": "HASH"},
{"AttributeName": "orderDate", "KeyType": "RANGE"}
],
"Projection": {
"ProjectionType": "ALL"
}
}
]' \
--billing-mode PAY_PER_REQUESTaws dynamodb update-table \
--table-name Orders \
--attribute-definitions \
AttributeName=status,AttributeType=S \
--global-secondary-index-updates '[
{
"Create": {
"IndexName": "StatusIndex",
"KeySchema": [
{"AttributeName": "status", "KeyType": "HASH"},
{"AttributeName": "orderDate", "KeyType": "RANGE"}
],
"Projection": {
"ProjectionType": "INCLUDE",
"NonKeyAttributes": ["customerId", "total"]
}
}
}
]'Adding a GSI takes time as DynamoDB backfills data. Monitor progress in the console or with describe-table.
GSI Projection Types
| Type | Attributes Included | Storage | Use Case |
|---|---|---|---|
| ALL | All attributes | Most | Need all data from queries |
| KEYS_ONLY | Only key attributes | Least | Just need to find items |
| INCLUDE | Keys + specified | Medium | Specific attributes needed |
"Projection": {
"ProjectionType": "INCLUDE",
"NonKeyAttributes": ["name", "email", "total"]
}Unprojected attributes require a separate GetItem on the base table—doubling read costs.
Querying a GSI
aws dynamodb query \
--table-name Orders \
--index-name CustomerIndex \
--key-condition-expression "customerId = :cid AND orderDate BETWEEN :start AND :end" \
--expression-attribute-values '{
":cid": {"S": "customer-123"},
":start": {"S": "2024-01-01"},
":end": {"S": "2024-01-31"}
}'import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { QueryCommand, DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
const client = DynamoDBDocumentClient.from(new DynamoDBClient({}));
const result = await client.send(new QueryCommand({
TableName: "Orders",
IndexName: "CustomerIndex",
KeyConditionExpression: "customerId = :cid",
ExpressionAttributeValues: {
":cid": "customer-123"
}
}));GSI Capacity
GSIs have their own capacity settings:
"GlobalSecondaryIndexes": [
{
"IndexName": "CustomerIndex",
"KeySchema": [...],
"Projection": {"ProjectionType": "ALL"},
"ProvisionedThroughput": {
"ReadCapacityUnits": 10,
"WriteCapacityUnits": 5
}
}
]If GSI capacity is insufficient, writes to the base table can be throttled—even if table capacity is available.
Updating GSI Capacity
aws dynamodb update-table \
--table-name Orders \
--global-secondary-index-updates '[
{
"Update": {
"IndexName": "CustomerIndex",
"ProvisionedThroughput": {
"ReadCapacityUnits": 20,
"WriteCapacityUnits": 10
}
}
}
]'Deleting a GSI
aws dynamodb update-table \
--table-name Orders \
--global-secondary-index-updates '[
{
"Delete": {
"IndexName": "StatusIndex"
}
}
]'Local Secondary Indexes (LSI)
Creating an LSI
LSIs must be created with the table:
aws dynamodb create-table \
--table-name Posts \
--attribute-definitions \
AttributeName=userId,AttributeType=S \
AttributeName=postId,AttributeType=S \
AttributeName=createdAt,AttributeType=S \
AttributeName=category,AttributeType=S \
--key-schema \
AttributeName=userId,KeyType=HASH \
AttributeName=postId,KeyType=RANGE \
--local-secondary-indexes '[
{
"IndexName": "UserPostsByDate",
"KeySchema": [
{"AttributeName": "userId", "KeyType": "HASH"},
{"AttributeName": "createdAt", "KeyType": "RANGE"}
],
"Projection": {"ProjectionType": "ALL"}
},
{
"IndexName": "UserPostsByCategory",
"KeySchema": [
{"AttributeName": "userId", "KeyType": "HASH"},
{"AttributeName": "category", "KeyType": "RANGE"}
],
"Projection": {"ProjectionType": "KEYS_ONLY"}
}
]' \
--billing-mode PAY_PER_REQUESTQuerying an LSI
aws dynamodb query \
--table-name Posts \
--index-name UserPostsByDate \
--key-condition-expression "userId = :uid AND createdAt > :date" \
--expression-attribute-values '{
":uid": {"S": "user-123"},
":date": {"S": "2024-01-01"}
}'LSI with Strong Consistency
LSIs support strongly consistent reads:
aws dynamodb query \
--table-name Posts \
--index-name UserPostsByDate \
--key-condition-expression "userId = :uid" \
--expression-attribute-values '{":uid": {"S": "user-123"}}' \
--consistent-readGSIs only support eventually consistent reads. Use LSI when strong consistency is required.
LSI Partition Size Limit
Each partition key value is limited to 10 GB of data across the base table and all LSIs:
Total Size = Base Table Items + All LSI Items ≤ 10 GB
(for same partition key)If you exceed 10 GB per partition key, writes fail with an ItemCollectionSizeLimitExceededException.
Index Design Patterns
Inverted Index
Create a GSI that swaps the partition and sort keys:
Base Table:
PK: userId SK: orderId
GSI (Inverted):
PK: orderId SK: userIdUse case: Query orders by orderId without knowing the userId.
Sparse Index
Index only includes items with the indexed attribute:
Base Table:
userId | status | isActive
user-1 | active | true
user-2 | inactive | (no attribute)
user-3 | active | true
GSI on isActive:
Only includes user-1 and user-3aws dynamodb query \
--table-name Users \
--index-name ActiveUsersIndex \
--key-condition-expression "isActive = :true" \
--expression-attribute-values '{":true": {"S": "true"}}'Use case: Efficiently query a subset of items (active users, pending orders).
Overloaded GSI
Use a generic attribute name to support multiple access patterns:
Base Table:
PK | SK | GSI1PK | GSI1SK
USER#123 | PROFILE | USER#123 | PROFILE
USER#123 | ORDER#001 | ORDER#001 | USER#123
ORDER#001 | ORDER#001 | USER#123 | 2024-01-15
GSI1: Query by GSI1PK/GSI1SKUse case: Single-table design with multiple entity types.
Composite Sort Key in GSI
Combine multiple attributes in the GSI sort key:
GSI Sort Key: STATUS#DATE#ORDERID
Examples:
PENDING#2024-01-15#order123
SHIPPED#2024-01-14#order456
DELIVERED#2024-01-13#order789aws dynamodb query \
--table-name Orders \
--index-name StatusDateIndex \
--key-condition-expression "pk = :pk AND begins_with(sk, :prefix)" \
--expression-attribute-values '{
":pk": {"S": "ORDERS"},
":prefix": {"S": "PENDING#2024-01"}
}'Index Limits
| Limit | Value |
|---|---|
| GSIs per table | 20 |
| LSIs per table | 5 |
| Attributes in all indexes | 100 |
| Projected attributes (INCLUDE) | 100 per index |
| LSI partition size | 10 GB |
Cost Considerations
Index Storage and Throughput
- GSIs consume storage (replicated data)
- GSI writes are eventually consistent and asynchronous
- Each item written to base table causes a write to each GSI
- Unprojected attribute fetches cost extra RCUs
Estimating GSI Costs
GSI Write Cost = Base Table Writes × Number of GSIs × Item Size Factor
Example:
- 1,000 writes/second to base table
- 2 GSIs
- Average item: 2 KB
Cost = 1,000 × 2 × 2 = 4,000 WCUs for GSIs
(Plus 2,000 WCUs for base table = 6,000 total)Managing Indexes
aws dynamodb describe-table \
--table-name Orders \
--query '{
TableName: Table.TableName,
GSIs: Table.GlobalSecondaryIndexes[*].{
Name: IndexName,
Status: IndexStatus,
Keys: KeySchema
},
LSIs: Table.LocalSecondaryIndexes[*].{
Name: IndexName,
Keys: KeySchema
}
}'Index Write Sharding
For high-write GSIs with low cardinality keys:
const getShardedKey = (status, shardCount = 10) => {
const shard = Math.floor(Math.random() * shardCount);
return `${status}#${shard}`;
};
// Write item
const item = {
orderId: "order-123",
status: "pending",
gsiPk: getShardedKey("pending") // "pending#7"
};
// Query all shards
const queryAllShards = async (status, shardCount = 10) => {
const promises = Array.from({ length: shardCount }, (_, i) =>
client.send(new QueryCommand({
TableName: "Orders",
IndexName: "StatusIndex",
KeyConditionExpression: "gsiPk = :pk",
ExpressionAttributeValues: { ":pk": `${status}#${i}` }
}))
);
return Promise.all(promises);
};Best Practices
Index Best Practices
- Design indexes for access patterns - Each GSI serves specific queries
- Use sparse indexes - Only index items that need to be queried
- Project only needed attributes - Reduce storage and costs
- Monitor GSI throttling - Ensure adequate capacity
- Limit number of GSIs - More GSIs = more write costs
- Consider LSI for consistency - When strong consistency is needed
- Use composite sort keys - For flexible range queries
- Avoid scanning indexes - Design for queries, not scans
Troubleshooting
| Issue | Cause | Solution |
|---|---|---|
| GSI throttling | Insufficient capacity | Increase GSI RCU/WCU |
| Base table throttled | GSI write backpressure | Increase GSI WCU |
| GSI not updating | Async replication | Wait, or check status |
| ItemCollectionSizeLimitExceeded | LSI limit | Redesign partition key |
| Query returns partial results | LastEvaluatedKey | Paginate results |