DevDocsDev Docs
DynamoDB

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
FeatureGSILSI
Partition KeyDifferent from tableSame as table
Sort KeyOptionalRequired
CreationAnytimeTable creation only
CapacitySeparate from tableShares with table
ConsistencyEventually consistentEventual or Strong
SizeUnlimited10 GB per partition

Global Secondary Indexes (GSI)

Creating a GSI

Create table with 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_REQUEST
Add GSI to existing table
aws 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

TypeAttributes IncludedStorageUse Case
ALLAll attributesMostNeed all data from queries
KEYS_ONLYOnly key attributesLeastJust need to find items
INCLUDEKeys + specifiedMediumSpecific attributes needed
INCLUDE projection
"Projection": {
  "ProjectionType": "INCLUDE",
  "NonKeyAttributes": ["name", "email", "total"]
}

Unprojected attributes require a separate GetItem on the base table—doubling read costs.

Querying a GSI

Query 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"}
  }'
Query GSI with SDK
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:

GSI with provisioned capacity
"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

Update GSI capacity
aws dynamodb update-table \
  --table-name Orders \
  --global-secondary-index-updates '[
    {
      "Update": {
        "IndexName": "CustomerIndex",
        "ProvisionedThroughput": {
          "ReadCapacityUnits": 20,
          "WriteCapacityUnits": 10
        }
      }
    }
  ]'

Deleting a GSI

Delete 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:

Create table with LSI
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_REQUEST

Querying an LSI

Query 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:

Strongly consistent LSI query
aws dynamodb query \
  --table-name Posts \
  --index-name UserPostsByDate \
  --key-condition-expression "userId = :uid" \
  --expression-attribute-values '{":uid": {"S": "user-123"}}' \
  --consistent-read

GSIs 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: userId

Use 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-3
Query sparse index
aws 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/GSI1SK

Use 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#order789
Query by status and date range
aws 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

LimitValue
GSIs per table20
LSIs per table5
Attributes in all indexes100
Projected attributes (INCLUDE)100 per index
LSI partition size10 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

Describe table with 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:

Write sharding
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

  1. Design indexes for access patterns - Each GSI serves specific queries
  2. Use sparse indexes - Only index items that need to be queried
  3. Project only needed attributes - Reduce storage and costs
  4. Monitor GSI throttling - Ensure adequate capacity
  5. Limit number of GSIs - More GSIs = more write costs
  6. Consider LSI for consistency - When strong consistency is needed
  7. Use composite sort keys - For flexible range queries
  8. Avoid scanning indexes - Design for queries, not scans

Troubleshooting

IssueCauseSolution
GSI throttlingInsufficient capacityIncrease GSI RCU/WCU
Base table throttledGSI write backpressureIncrease GSI WCU
GSI not updatingAsync replicationWait, or check status
ItemCollectionSizeLimitExceededLSI limitRedesign partition key
Query returns partial resultsLastEvaluatedKeyPaginate results

Next Steps

On this page