AWS Database Blog

Multi-key support for Global Secondary Index in Amazon DynamoDB

Amazon DynamoDB has announced support for up to 8 attributes in composite keys for Global Secondary Indexes (GSIs). Now, you can specify up to four partition keys and four sort keys to identify items as part of a GSI, allowing you to query data at scale across multiple dimensions.

Amazon DynamoDB is a serverless, fully managed, distributed NoSQL database with single-digit millisecond performance at any scale. A key feature of DynamoDB is its schema flexibility. Apart from the primary key, everything else is optional. You can start with a simple table that has a partition key (PK) and, optionally, a sort key (SK), both as string attributes, and you are ready to build your data model.

Sometimes applications need to use more than one attribute in the partition key to increase cardinality. Similarly, with the sort key when writing and querying data, to provide additional flexibility in how data is filtered. For example, if an access pattern requires retrieving transactions by status and date, or sensor readings filtered by both reading type and timestamp, using two attributes in the sort key greatly increases application efficiency. This is where you had to get creative with your solutions, concatenating attributes together at the application side to create a composite of two or more attributes.

In this post we show you how to design similar data models more efficiently using Global Secondary Indexes with the additional attribute support in composite keys and provide examples of DynamoDB data models with reduced complexity.

Understanding partition and sort keys

The partition key represents the known element in your queries, it is basically the user_id, account_id, sensor_id, player_id, and more; the identifier you need to use while retrieving data. Without this information, finding items in your table is less efficient. The sort key provides answers to questions about items that share the same partition key. For example, “Find transactions by date for this account.” or “Show sensor readings and alarms for this device over time?” or “What items are in this customer’s shopping cart?

With the new GSIs support for up to four partition keys and four sort keys, your approach to multi-dimensional queries can change. By using this capability, developer workarounds are no longer needed, maintaining the performance of DynamoDB and further increasing its flexibility.

Core concepts

DynamoDB tables are built around two concepts:

  • The partition key is the main identifier, which dictates where your data is stored. This represents the “known element” in your access pattern. The partition key must be provided in any API operation that is efficient, such as the query API.
  • The sort key is an optional attribute for you to store related items together and query them efficiently using range operations. Using sort keys makes one-to-many relationships possible and generates item collections (items which have the same partition key).

You can combine different entity types in the sort key, such as entity type, status and timestamp in ISO8601 by concatenating them using the # character and leverage the sort key conditions to retrieve specific queries. Let’s see this example where you will retrieve information from the customer C#1A2B3C.

  • To retrieve all the orders. Use the sort key condition BEGINS_WITH ORDER#
  • If you only want to get the PENDING orders you can also use the sort key condition BEGINS_WITH including the status you are looking for ORDER#PENDING.
  • Finally, if you want to retrieve all the pending orders in between two dates, use the sort key condition BETWEEN ORDER#PENDING#2025-11-01 AND ORDER#PENDING#2025-11-04. Notice how every time we provide more details to retrieve a smaller subset of data.

When designing your data model, the partition key answers, “What am I looking for?” or “What is the main thing” in other words the must-know information to locate the data. The sort key answers, “What aspects of the thing I want?” or “What details helps me to narrow it down?” in other words how do I organize and filter the data?

For example: For the customer 1A2B3C, which items are active in the shopping cart?

The must-know information is the customer 1A2B3C. This is the main thing you are looking for. You can’t find any shopping cart items without knowing which customer they belong to.

How do I organize and filter the data? active items in the shopping cart. You will organize the items by ACTIVE status in the shopping cart, adding the SKU for uniqueness.

The following table shows a sample data model with three shopping cart items for the customer 1A2B3C:

Partition Key Sort Key status sku date
C#1A2B3C CART#S#ACTIVE#SKU#123ABC ACTIVE 123ABC 2025-11-04T10:00:00
C#1A2B3C CART#S#ACTIVE#SKU#234BCD ACTIVE 234BCD 2025-11-04T10:05:00
C#1A2B3C CART#S#SAVED#SKU#345CDE SAVED 345CDE 2025-11-04T10:08:00

When you need to query across multiple attributes in your sort key, the common pattern was to concatenate into a composite sort key. For example:


PK: C#1A2B3C
SK: ORDER#PENDING#2024-11-01T10:30:00Z

Introducing expanded composite keys for Global Secondary Indexes

Global Secondary Indexes support multi-attribute keys, allowing you to compose partition keys and sort keys from multiple attributes. Each attribute maintains its own data type (string, number or binary) providing flexible querying capabilities. GSIs are sparse, If any component of a composite key is missing, items won’t be indexed, like with single keys GSI.

  • Multiple partition keys: Combine up to four attributes as partition keys (example: tenant, customer, department).
  • Multiple sort keys: Define up to four sort key attributes with specific query patterns.
  • Native data types: Each attribute keeps its type, and no string conversion and concatenation is required.
  • Effective querying: Query with increasingly specific attribute combinations, without restructuring your data.
  • Simple sharding techniques: Use two or more partition keys to reduce the risk of having hot partitions. Solve them by implementing an intelligent sharding technique, find information in your data model to distribute your traffic.

The following query patterns are supported:

  • Queries require equality conditions (=) on all partition key attributes.
  • Sort key conditions are optional and can use up to 4 attributes with equality (=) conditions.
  • Range conditions (<, >, <=, >=, BETWEEN, BEGINS_WITH) are only supported on the last sort key attribute.
  • You can’t skip sort keys in your query; for example, a query with all the partition keys but only the first and third sort keys only is not supported.
  • You can provide partial sort keys from left to right. For example, a query where you specify only the first sort key, or only the first and the second sort key is supported.
  • Maintain the same DynamoDB performance while reducing application complexity.

How composite keys work

Let’s walk through a complete example to see expanded composite keys for GSIs in action.

Scenario 1: Orders dashboard

You are building a system that tracks orders, it has the following requirements:

  • The system tracks the orders by order id to update its status and metadata.
  • Users can query their orders by status (ACTIVE, PENDING, COMPLETED) with amount thresholds, such as, ACTIVE orders on November 4th greater than $100.

Base table design

From the two access patterns we have in the Scenario 1, only one is not related to the user. Use the tracking system for the base table, it will allow you to scale the system as the order is the most atomic piece of information.

The must-know information is the order_id. Without an order_id you will have to scan the entire table. To help organize orders by date for the other access patterns, use a K-sortable unique identifier (KSUID), which is sortable by generation time.

Since this example doesn’t include any query on the order id, there is no need for a sort key. The following table shows a sample data model with three orders for the customer 1A2B3C:

Partition Key
order_id customer_id order_date amount status acc_type org_id
KSUID1 C#1A2B3C 2025-11-04 200 ACTIVE A OMEGA
KSUID2 C#1A2B3C 2025-11-04 145 PENDING A OMEGA
KSUID3 C#1A2B3C 2025-11-04 110 PENDING B BRAVO
Base Table: Orders
Partition Key: order_id
Attributes: customer_id, status, order_date, amount, acc_type and org_id

Composite keys GSI design

For the dashboard queries, you create a GSI where customer_id is the partition key, and the sort key is composed of status, order_date, and amount. Since inequalities need to be the last sort key attribute, positioning amount last means you can use range queries to filter by price thresholds.

The must-know information is the customer 1A2B3C. You can’t find any order without knowing which customer it belongs to. How do I organize and filter the data? You need to organize and filter by active orders for a given date that are greater than $100. Using a composite key using status and date, you can retrieve the orders for this customer.

The following table shows a sample data model with three orders for the customer 1A2B3C:

GSI PK GSI SK
customer_id status order_date amount order_id acc_type org_id
C#1A2B3C ACTIVE 2025-11-04 200 KSUID1 A OMEGA
C#1A2B3C PENDING 2025-11-04 145 KSUID2 A OMEGA
C#1A2B3C PENDING 2025-11-04 110 KSUID3 B BRAVO
GSI: OrdersByStatusDateAmount
Partition Key: customer_id
Sort Key 1: status (equality condition)
Sort Key 2: order_date (equality condition)
Sort Key 3: amount (range condition)

In the following AWS CLI command, you will retrieve the orders for the customer 1A2B3C.

aws dynamodb query \
    --table-name orders-table \
    --index-name OrdersByStatusDateAmount \
    --key-condition-expression "customer_id = :cust" \
    --expression-attribute-values '{
        ":cust": {"S": "1A2B3C"}
    }'
{
    "Items": [
        {
            "org_id": {"S": "OMEGA"},
            "order_date": {"S": "2025-11-04"},
            "status": {"S": "ACTIVE"},
            "acc_type": {"S": "A"},
            "customer_id": {"S": "1A2B3C"},
            "amount": {"N": "200"},
            "order_id": {"S": "KSUID1"}
        },
        {
            "org_id": {"S": "BRAVO"},
            "order_date": {"S": "2025-11-04"},
            "status": {"S": "PENDING"},
            "acc_type": {"S": "B"},
            "customer_id": {"S": "1A2B3C"},
            "amount": {"N": "110"},
            "order_id": {"S": "KSUID3"}
        },
        {
            "org_id": {"S": "OMEGA"},
            "order_date": {"S": "2025-11-04"},
            "status": {"S": "PENDING"},
            "acc_type": {"S": "A"},
            "customer_id": {"S": "1A2B3C"},
            "amount": {"N": "145"},
            "order_id": {"S": "KSUID2"}
        }
    ],
    "Count": 3,
    "ScannedCount": 3,
    "ConsumedCapacity": null
}

The following AWS CLI command queries the orders for the customer 1A2B3C that are in a PENDING status:

aws dynamodb query \
    --table-name orders-table \
    --index-name OrdersByStatusDateAmount \
    --key-condition-expression "customer_id = :cust AND #status = :status" \
    --expression-attribute-names '{"#status": "status"}' \
    --expression-attribute-values '{
        ":cust": {"S": "1A2B3C"},
        ":status": {"S": "PENDING"}
    }' 

The following AWS CLI command retrieves the orders for the customer 1A2B3C that are in a PENDING status on November 4th:

aws dynamodb query \
    --table-name orders-table \
    --index-name OrdersByStatusDateAmount \
    --key-condition-expression "customer_id = :cust AND #status = :status AND order_date = :date" \
    --expression-attribute-names '{"#status": "status"}' \
    --expression-attribute-values '{
        ":cust": {"S": "1A2B3C"},
        ":status": {"S": "PENDING"},
        ":date": {"S": "2025-11-04"}
    }'

The following AWS CLI command queries the orders for the customer 1A2B3C that are in a PENDING status on November 4th with an amount greater than $100.

aws dynamodb query \
    --table-name orders-table \
    --index-name OrdersByStatusDateAmount \
    --key-condition-expression "customer_id = :cust AND #status = :status AND order_date = :date AND amount > :min_amount" \
    --expression-attribute-names '{"#status": "status"}' \
    --expression-attribute-values '{
        ":cust": {"S": "1A2B3C"},
        ":status": {"S": "PENDING"},
        ":date": {"S": "2025-11-04"},
        ":min_amount": {"N": "100"}
    }'

Scenario 2: Evolution – growing in traffic

Now imagine the solution is a success and your largest enterprise customers will push order statuses at a rate that is higher than 500 times per second. The requirements remain the same, users need to query their order by status within some amount thresholds.

With the introduction of this large enterprise customer, we are facing the possibility of hot partitions. Updating one order status from NEW to ACTIVE, in the base table, will use a single write operation consuming one WCU; With the GSI, it will consume two WCUs, one to delete the NEW status and another to set it to ACTIVE, approaching to the 1000 WCU limit per virtual partition.

Fortunately, we can use the status attribute as a sharding key, assuming that we will have five different statuses you can increase the throughput by up to 5x. Create a new GSI where the partition key is customer_id and status, and the sort key is order_date and amount.

GSI PK GSI SK
customer_id status order_date amount order_id acc_type org_id
C#1A2B3C ACTIVE 2025-11-04 200 KSUID1 A OMEGA
C#1A2B3C PENDING 2025-11-04 145 KSUID2 A OMEGA
C#1A2B3C PENDING 2025-11-04 110 KSUID3 B BRAVO
GSI: OrdersByOrgAccountStatus
Partition Key 1: customer_id
Partition Key 2: status
Sort Key 2: order_date (equality condition)
Sort Key 3: amount (range condition)

Best practices for using multi-attribute composite key

Design for your query patterns first. Before creating GSIs, identify your top 3-5 most popular query patterns, understand their frequency and performance requirements. Design your base table structure for uniqueness and use the GSI to optimize for composite key patterns, ensuring that your most common queries can be satisfied efficiently. With composite keys GSIs, you can adapt to new requirements by adding indexes with different key combinations, rather than restructuring your entire data model.

Choose your key order carefully. The order of attributes in your GSI directly impacts which queries you can run. You don’t need to use four partition keys and four sort keys, choose the combination that matches your access patterns, whether that’s three partition keys with two sort keys, one partition key with three sort keys, or any other configuration.

One of the most powerful aspects of composite keys GSIs is the support for native data types. Use Number types for timestamps, quantities, and numeric comparisons to enable proper sorting and mathematical operations. Use Boolean flags for binary states like active/inactive or enabled/disabled. Avoid converting values to strings unless necessary, as this eliminates the benefits of type-specific operations.

Plan for scale from the start. Design sparse indexes whenever possible to reduce costs by only indexing items that have values for your specified key attributes. Choose your projection type strategically: use ALL for flexibility, KEYS_ONLY to reduce storage costs, or INCLUDE to project only the attributes you need. Implement time to live (TTL) strategies in your base table to manage record size over time and prevent unbounded growth.

Conclusion and next steps

In this post you learned how to work with composite keys GSIs. With this new DynamoDB feature you can query different data types and attributes without needing to use workarounds like concatenating attributes. It is available now on all DynamoDB tables at no additional cost beyond standard GSI storage, throughput and features.Ready to simplify your DynamoDB data model? Follow these steps:

  1. Identify your query patterns: Review which queries currently use concatenated sort keys
  2. Design your GSI: Map your attributes to partition and sort key positions based on data hierarchies.
  3. Test thoroughly: Create a test table and validate your query patterns work as expected under anticipated load.
  4. Deploy to production: Add the new GSI to your production table and update your application code
  5. Monitor performance: Track GSI metrics in CloudWatch and optimize as needed
  6. Clean up: Remove legacy composite attributes that are used in the GSI.

For detailed implementation guidance, see the How to use global secondary indexes in DynamoDB and Best Practices for Data Modeling.


About the authors

Esteban Serna

Esteban Serna

Esteban is a Principal DynamoDB Specialist Solutions Architect at AWS. He’s a database enthusiast with 16 years of experience. From deploying contact center infrastructure to falling in love with NoSQL, Esteban’s journey led him to specialize in distributed computing. Passionate about his work, Esteban loves nothing more than sharing his knowledge with others.