Overloaded GSI
In relational databases, you create a new index for every column you want to query. If you have 10 query patterns, you create 10 indexes.
In DynamoDB, indexes are expensive. Each Global Secondary Index (GSI) increases your storage cost and, more importantly, your Write Capacity Unit (WCU) consumption. Every time you write to the base table, DynamoDB has to replicate that write to every GSI where the item appears.
Index Overloading is a design pattern that allows you to use a single GSI to satisfy multiple access patterns across different entity types. This is the cornerstone of Single Table Design.
1. How It Works
Instead of creating specific attribute names like EmailAddress or OrderId for your GSI keys, you use generic names:
- Partition Key:
GSI1PK - Sort Key:
GSI1SK
You then map different attributes to these generic keys depending on the type of item (Entity) you are storing.
Example: E-Commerce System
Imagine we have Users, Orders, and Products in a Single Table.
- Access Pattern A: Get User by Email.
- Access Pattern B: Get Orders by Status.
- Access Pattern C: Get Products by Category.
Instead of creating 3 GSIs (EmailIndex, StatusIndex, CategoryIndex), we create one GSI (GSI1) and overload it.
| Entity | Base Table PK | Base Table SK | GSI1PK (Partition) | GSI1SK (Sort) |
|---|---|---|---|---|
| User | USER#123 |
PROFILE |
EMAIL#alice@ex.com |
USER#123 |
| Order | ORDER#999 |
META |
STATUS#SHIPPED |
2023-10-27 |
| Product | PROD#555 |
META |
CAT#ELECTRONICS |
PRICE#99.99 |
Now, we can support all three queries using the same index:
- Find User by Email: Query GSI1 where
PK = "EMAIL#alice@ex.com" - Find Shipped Orders: Query GSI1 where
PK = "STATUS#SHIPPED" - Find Electronics: Query GSI1 where
PK = "CAT#ELECTRONICS"
2. Interactive: GSI Overload Simulator
See how different entities map to the same underlying index structure.
| Entity Type | GSI1PK (Partition Key) | GSI1SK (Sort Key) | Query Use Case |
|---|
3. The “Adjacency List” Pattern
This concept is often used to model Many-to-Many relationships, known as the Adjacency List Pattern.
If User and Group have a many-to-many relationship:
- Base Table: Stores the relationship.
- PK:
USER#123 - SK:
GROUP#456
- PK:
- Inverted Index (GSI1): Flips the relationship.
- GSI1PK:
GROUP#456(from SK) - GSI1SK:
USER#123(from PK)
- GSI1PK:
Now you can query:
- Base Table: “What groups does User 123 belong to?”
- GSI: “What users belong to Group 456?”
4. Code Implementation
Go: Structuring for Overloading
Here is how you structure your struct to support overloading. Note the GSI1PK and GSI1SK fields are used for different purposes depending on the entity.
package main
import (
"context"
"fmt"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
)
// GenericItem represents any entity in our Single Table
type GenericItem struct {
PK string // Base Table Partition Key
SK string // Base Table Sort Key
Type string // "User", "Order", "Product"
// Overloaded Index Keys
GSI1PK string
GSI1SK string
// Data Attributes
Email string
Price float64
Category string
}
func PutUser(client *dynamodb.Client, table string, email, userId string) {
// For User: GSI1PK = Email, GSI1SK = UserId
item := map[string]types.AttributeValue{
"PK": &types.AttributeValueMemberS{Value: fmt.Sprintf("USER#%s", userId)},
"SK": &types.AttributeValueMemberS{Value: "PROFILE"},
"Type": &types.AttributeValueMemberS{Value: "User"},
"GSI1PK": &types.AttributeValueMemberS{Value: fmt.Sprintf("EMAIL#%s", email)},
"GSI1SK": &types.AttributeValueMemberS{Value: fmt.Sprintf("USER#%s", userId)},
"Email": &types.AttributeValueMemberS{Value: email},
}
_, err := client.PutItem(context.TODO(), &dynamodb.PutItemInput{
TableName: aws.String(table),
Item: item,
})
if err != nil { panic(err) }
fmt.Printf("Put User: %s\n", email)
}
Java: Querying the Overloaded Index
When querying, you simply provide the key pattern that matches the entity you are looking for.
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.services.dynamodb.model.*;
import java.util.HashMap;
import java.util.Map;
public class OverloadedQuery {
public static void findUserByEmail(DynamoDbClient ddb, String tableName, String email) {
Map<String, AttributeValue> attrValues = new HashMap<>();
attrValues.put(":email", AttributeValue.builder().s("EMAIL#" + email).build());
QueryRequest queryReq = QueryRequest.builder()
.tableName(tableName)
.indexName("GSI1")
.keyConditionExpression("GSI1PK = :email")
.expressionAttributeValues(attrValues)
.build();
QueryResponse response = ddb.query(queryReq);
System.out.println("User found: " + response.count());
}
public static void findOrdersByStatus(DynamoDbClient ddb, String tableName, String status) {
Map<String, AttributeValue> attrValues = new HashMap<>();
attrValues.put(":status", AttributeValue.builder().s("STATUS#" + status).build());
QueryRequest queryReq = QueryRequest.builder()
.tableName(tableName)
.indexName("GSI1")
.keyConditionExpression("GSI1PK = :status")
.expressionAttributeValues(attrValues)
.build();
QueryResponse response = ddb.query(queryReq);
System.out.println("Orders found: " + response.count());
}
}
[!TIP] Pro Tip: Always project your
Typeattribute into your GSI. This helps when debugging or if you ever accidentally mix entity types in the same GSI partition.