---
title: "Customize secondary indexes"
section: "build-a-backend/data/data-modeling"
platforms: ["android", "angular", "flutter", "javascript", "nextjs", "react", "react-native", "swift", "vue"]
gen: 2
last-updated: "2026-02-09T14:57:32.000Z"
url: "https://docs.amplify.aws/react/build-a-backend/data/data-modeling/secondary-index/"
---

// cspell:ignore ACCOUNTREPRESENTATIVEID
You can optimize your list queries based on "secondary indexes". For example, if you have a **Customer** model, you can query based on the customer's **id** identifier field by default but you can add a secondary index based on the **accountRepresentativeId** to get list customers for a given account representative.

A secondary index consists of a "hash key" and, optionally, a "sort key". Use the "hash key" to perform strict equality and the "sort key" for greater than (gt), greater than or equal to (ge), less than (lt), less than or equal to (le), equals (eq), begins with, and between operations.

```ts title="amplify/data/resource.ts"
export const schema = a.schema({
  Customer: a
    .model({
      name: a.string(),
      phoneNumber: a.phone(),
      accountRepresentativeId: a.id().required(),
    })
      // highlight-next-line
    .secondaryIndexes((index) => [index("accountRepresentativeId")])
    .authorization(allow => [allow.publicApiKey()]),
});
```

<!-- Platform: javascript, angular, react-native, react, nextjs, vue, android -->
The example client query below allows you to query for "Customer" records based on their `accountRepresentativeId`:

```ts title="src/App.tsx"
import { type Schema } from '../amplify/data/resource';
import { generateClient } from 'aws-amplify/data';

const client = generateClient<Schema>();

const { data, errors } =
  // highlight-start
  await client.models.Customer.listCustomerByAccountRepresentativeId({
    accountRepresentativeId: "YOUR_REP_ID",
  });
  // highlight-end
```
<!-- /Platform -->

<!-- Platform: swift -->
The example client query below creates a custom GraphQL request that allows you to query for "Customer" records based on their `accountRepresentativeId`:

```swift
struct PaginatedList<ModelType: Model>: Decodable {
    let items: [ModelType]
    let nextToken: String?
}
let operationName = "listCustomer8ByAccountRepresentativeId"
let document = """
query ListCustomer8ByAccountRepresentativeId {
  \(operationName)(accountRepresentativeId: "\(accountRepresentativeId)") {
    items {
      createdAt
      accountRepresentativeId
      id
      name
      phoneNumber
      updatedAt
    }
    nextToken
  }
}
"""

let request = GraphQLRequest<PaginatedList<Customer>>(
    document: document,
    responseType: PaginatedList<Customer>.self,
    decodePath: operationName)

let queriedCustomers = try await Amplify.API.query(
    request: request).get()
```
<!-- /Platform -->

<!-- Platform: flutter -->
The example client query below allows you to query for "Customer" records based on their `accountRepresentativeId`:

```dart title="lib/main.dart"
import 'package:amplify_api/amplify_api.dart';
import 'package:amplify_flutter/amplify_flutter.dart';
import 'models/ModelProvider.dart';

// highlight-start
final request = ModelQueries.list(
  Customer.classType,
  where: Customer.ACCOUNTREPRESENTATIVEID.eq(YOUR_REP_ID),
);
// highlight-end

```
<!-- /Platform -->

<details><summary>Review how this works under the hood with Amazon DynamoDB</summary>

Amplify uses Amazon DynamoDB tables as the default data source for `a.model()`. For key-value databases, it is critical to model your access patterns with "secondary indexes". Use the `.secondaryIndexes()` modifier to configure a secondary index.

**Amazon DynamoDB** is a key-value and document database that delivers single-digit millisecond performance at any scale but making it work for your access patterns requires a bit of forethought. DynamoDB query operations may use at most two attributes to efficiently query data. The first query argument passed to a query (the hash key) must use strict equality and the second attribute (the sort key) may use gt, ge, lt, le, eq, beginsWith, and between. DynamoDB can effectively implement a wide variety of access patterns that are powerful enough for the majority of applications.

</details>

## Add sort keys to secondary indexes

You can define "sort keys" to add a set of flexible filters to your query, such as "greater than" (gt), "greater than or equal to" (ge), "less than" (lt), "less than or equal to" (le), "equals" (eq), "begins with" (beginsWith), and "between" operations.

```ts title="amplify/data/resource.ts"
export const schema = a.schema({
  Customer: a
    .model({
      name: a.string(),
      phoneNumber: a.phone(),
      accountRepresentativeId: a.id().required(),
    })
    .secondaryIndexes((index) => [
      index("accountRepresentativeId")
      // highlight-next-line
        .sortKeys(["name"]),
    ])
    .authorization(allow => [allow.owner()]),
});
```

<!-- Platform: javascript, angular, react-native, react, nextjs, vue, android, -->
On the client side, you should find a new `listBy...` query that's named after hash key and sort keys. For example, in this case: `listByAccountRepresentativeIdAndName`. You can supply the filter as part of this new list query:

```ts title="src/App.tsx"
const { data, errors } =
  // highlight-next-line
  await client.models.Customer.listCustomerByAccountRepresentativeIdAndName({
    accountRepresentativeId: "YOUR_REP_ID",
    name: {
      beginsWith: "Rene",
    },
  });
```
<!-- /Platform -->

<!-- Platform: swift -->
On the client side, you can create a custom GraphQL request based on the new `listBy...` query that's named after hash key and sort keys. You can supply the filter as part of this new list query:

```swift
struct PaginatedList<ModelType: Model>: Decodable {
    let items: [ModelType]
    let nextToken: String?
}
let operationName = "listCustomer9ByAccountRepresentativeIdAndName"
let document = """
query ListCustomer8ByAccountRepresentativeId {
  \(operationName)(accountRepresentativeId: "\(accountRepresentativeId)", name: {beginsWith: "\(name)"}) {
    items {
      accountRepresentativeId
      createdAt
      id
      name
      phoneNumber
      updatedAt
    }
    nextToken
  }
}
"""
let request = GraphQLRequest<PaginatedList<Customer>>(
    document: document,
    responseType: PaginatedList<Customer>.self,
    decodePath: operationName)

let queriedCustomers = try await Amplify.API.query(
    request: request).get()
```
<!-- /Platform -->

<!-- Platform: flutter -->
The example client query below allows you to query for "Customer" records based on their `name` AND their `accountRepresentativeId`:

```dart title="lib/main.dart"
import 'package:amplify_api/amplify_api.dart';
import 'package:amplify_flutter/amplify_flutter.dart';
import 'models/ModelProvider.dart';

// highlight-start
final request = ModelQueries.list(
  Customer.classType,
  where: Customer.ACCOUNTREPRESENTATIVEID.eq(YOUR_REP_ID) & Customer.NAME.beginsWith("Rene"),
);
// highlight-end

```
<!-- /Platform -->

## Customize the query field for secondary indexes

You can also customize the auto-generated query name under `client.models.<MODEL_NAME>.listBy...` by setting the `queryField()` modifier.

```ts title="amplify/data/resource.ts"
const schema = a.schema({
  Customer: a
    .model({
      name: a.string(),
      phoneNumber: a.phone(),
      accountRepresentativeId: a.id().required(),
    })
    .secondaryIndexes((index) => [
      index("accountRepresentativeId")
        // highlight-next-line
        .queryField("listByRep"),
    ])
    .authorization(allow => [allow.owner()]),
});
```

<!-- Platform: javascript, angular, react-native, react, nextjs, vue, android -->
In your client app code, you'll see query updated under the Data client:

```ts title="src/App.tsx"
const {
  data,
  errors
  // highlight-next-line
} = await client.models.Customer.listByRep({
  accountRepresentativeId: 'YOUR_REP_ID',
})
```
<!-- /Platform -->

<!-- Platform: swift -->
In your client app code, you can use the updated query name.

```swift
struct PaginatedList<ModelType: Model>: Decodable {
    let items: [ModelType]
    let nextToken: String?
}
let operationName = "listByRep"
let document = """
query ListByRep {
  \(operationName)(accountRepresentativeId: "\(accountRepresentativeId)") {
    items {
      accountRepresentativeId
      createdAt
      id
      name
      phoneNumber
      updatedAt
    }
    nextToken
  }
}
"""

let request = GraphQLRequest<PaginatedList<Customer>>(
    document: document,
    responseType: PaginatedList<Customer>.self,
    decodePath: operationName)

let queriedCustomers = try await Amplify.API.query(
    request: request).get()
```
<!-- /Platform -->

<!-- Platform: flutter -->
In your client app code, you can use the updated query name.

```dart title="lib/main.dart"
import 'package:amplify_api/amplify_api.dart';
import 'package:amplify_flutter/amplify_flutter.dart';
import 'models/ModelProvider.dart';

var accountRepresentativeId = "John";
var operationName = "listByRep";
var document = """
  query ListByRep {
    $operationName(accountRepresentativeId: "$accountRepresentativeId") {
      items {
        accountRepresentativeId
        createdAt
        id
        name
        phoneNumber
        updatedAt
      }
      nextToken
    }
  }
  """;

final request = GraphQLRequest(
  document: document,
  variables: {
    'accountRepresentativeId': accountRepresentativeId,
    'operationName': operationName, 
  },
);
```
<!-- /Platform -->

## Customize the name of secondary indexes

To customize the underlying DynamoDB's index name, you can optionally provide the `name()` modifier.

```ts title="amplify/data/resource.ts"
const schema = a.schema({
  Customer: a
    .model({
      name: a.string(),
      phoneNumber: a.phone(),
      accountRepresentativeId: a.id().required(),
    })
    .secondaryIndexes((index) => [
      index("accountRepresentativeId")
        // highlight-next-line
        .name("MyCustomIndexName"),
    ])
    .authorization(allow => [allow.owner()]),
});
```

## Customize the projection type of secondary indexes

You can control which attributes are projected (copied) into your secondary indexes to reduce storage costs and optimize for your query patterns. Use the `.projection()` modifier to specify a projection type.

```ts title="amplify/data/resource.ts"
const schema = a.schema({
  Product: a
    .model({
      id: a.id().required(),
      name: a.string().required(),
      category: a.string().required(),
      price: a.float().required(),
      description: a.string(),
      inStock: a.boolean().required(),
    })
    .secondaryIndexes((index) => [
      // Project only keys - smallest index size
      // highlight-next-line
      index('category').projection('KEYS_ONLY'),
      
      // Project specific attributes you need to query in addition to the keys
      // highlight-next-line
      index('inStock').projection('INCLUDE', ['name', 'price']),
      
      // Project all attributes (default)
      // highlight-next-line
      index('name').projection('ALL'),
    ])
    .authorization(allow => [allow.publicApiKey()]),
});
```

<details><summary>Review projection types and when to use them</summary>

DynamoDB supports three projection types:

- **KEYS_ONLY** - Projects only index and primary keys. Smallest index size, lowest storage cost.
- **INCLUDE** - Projects keys plus specified attributes. Balances storage cost with query flexibility.
- **ALL** - Projects all attributes. Maximum query flexibility but highest storage cost (default).

Choose based on your query patterns and storage cost requirements. For more details, see [Projections in DynamoDB](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GSI.html#GSI.Projections).

</details>
