DynamoDB Query a GSI in Node.js (AWS SDK v3)
A GSI query is a normal Query plus IndexName, and then two things stop behaving like a table query: the consistency flag you are used to becomes an error, and the pagination cursor grows an extra attribute. Here AlbumTitle-index fetches songs by album, which the base table (Artist + SongTitle) cannot do without a scan.
Code
import {DynamoDBClient, QueryCommand} from '@aws-sdk/client-dynamodb';
const client = new DynamoDBClient({region: 'us-east-1'});
const items = [];
let lastEvaluatedKey;
do {
const response = await client.send(
new QueryCommand({
TableName: 'Music',
IndexName: 'AlbumTitle-index',
KeyConditionExpression: '#hashKey = :hashKeyValue',
ExpressionAttributeNames: {
'#hashKey': 'AlbumTitle'
},
ExpressionAttributeValues: {
':hashKeyValue': {S: 'Danzon'}
},
ExclusiveStartKey: lastEvaluatedKey
})
);
items.push(...(response.Items ?? []));
lastEvaluatedKey = response.LastEvaluatedKey;
} while (lastEvaluatedKey);
console.log(`Found ${items.length} songs on the album`);The cursor is three attributes wide, not two
Run the loop above against 300 songs on one album and inspect the LastEvaluatedKey it hands back:
table query -> ['Artist', 'SongTitle']
GSI query -> ['AlbumTitle', 'Artist', 'SongTitle']A GSI key is not unique, so the index key alone cannot resume a scan. DynamoDB returns the index key and the base-table key together, and both must go back in ExclusiveStartKey untouched. This is why a hand-rolled cursor that stores "the last sort key I saw" works on a table and quietly loses or repeats items on an index — and why persisting that key to a client is a bad idea when the table key is a user id you would rather not leak.
ConsistentRead: true is a 400, not an upgrade
The instinct is that a strongly consistent read costs more capacity and you get fresher data. On a GSI it costs you the request:
ValidationException: Consistent reads are not supported on global secondary indexes
HTTP 400The API reference is equally blunt: "Strongly consistent reads are not supported on global secondary indexes. If you query a global secondary index with ConsistentRead set to true, you will receive a ValidationException." Scan on a GSI rejects the same flag with the same message. Local secondary indexes do accept it.
Reproduced 2026-07-28 against DynamoDB Local (amazon/dynamodb-local) with @aws-sdk/client-dynamodb 3.1095.0 on node v24.18.0. The error text and key shapes are the engine's own output.
Explanation
IndexNamedoes not replaceTableName. Both go in the same command, and theKeyConditionExpressionthen names the index's partition key (AlbumTitle), not the table's, with the same operator set as a table query.- You get the projection and nothing else. A GSI query returns what the index projects (
ALL,KEYS_ONLY, or theINCLUDElist), and per the API reference "global secondary index queries cannot fetch attributes from the parent table". A missing attribute means a follow-upGetItemon the base key, or a wider projection and a re-created index. - Items missing the index key never appear. That is the sparse-index pattern, and it is a feature: index only the rows with
status = "OPEN"and the GSI stays small. It is also the reason a GSI query can return fewer items than you expect and raise no error. - Replication is asynchronous, so a write that just landed on the table may not be in the index yet. Budget for that in read-after-write paths rather than retrying in a tight loop.
Do it visually
Adding a GSI after the fact is the expensive way to learn this. The single-table design planner takes your access patterns and works out which of them need an index key and which the base table already serves.
To browse a table's indexes and run GSI queries from a form, with a paginated grid, download DynoTable.
Related examples
- DynamoDB Query a GSI in Python — the same index query with boto3.
- DynamoDB Query a GSI with the AWS CLI — the same index query from the shell.
- DynamoDB Query in Node.js — querying the base table.
- GSI vs. LSI — which index type fits the access pattern.
- Why GSIs are eventually consistent — the replication lag explained.
- "The table does not have the specified index" — the index name doesn't match (GSI names are case-sensitive).
- "Consistent reads are not supported on global secondary indexes" — why the consistent-read flag fails on a GSI.