Skip to content

Query and Scan

Mark Lauter edited this page May 16, 2026 · 5 revisions

Query and Scan

Overview

Query retrieves items by primary key with optional sort key conditions. Scan reads every item in a table or index. Both support filtering, projection, and pagination. See Querying and Scanning in the AWS docs.

QueryAsync

Retrieves items matching a partition key and optional sort key condition.

Signature

Task<QueryResponse> QueryAsync(QueryRequest request, CancellationToken ct = default)

Parameters

  • TableName (required) — target table
  • KeyConditionExpression (required) — partition key equality + optional sort key condition
  • FilterExpression — post-retrieval filter (doesn't reduce read cost)
  • ProjectionExpression — comma-separated attributes to return
  • ExpressionAttributeNames / ExpressionAttributeValues — expression substitutions
  • IndexName — query a secondary index (see Secondary Indexes)
  • ScanIndexForwardtrue (default) for ascending, false for descending sort key order
  • Limit — maximum number of items to evaluate (before filtering)
  • ExclusiveStartKey — resume pagination from this key
  • SelectALL_ATTRIBUTES (default), COUNT, or ALL_PROJECTED_ATTRIBUTES
  • ConsistentRead — accepted but has no effect on table queries (always consistent). Throws AmazonDynamoDBException when set to true on a GSI query.

KeyConditionExpression operators

The partition key must use equality (=). The sort key supports:

Operator Example
= SK = :val
< SK < :val
<= SK <= :val
> SK > :val
>= SK >= :val
BETWEEN ... AND ... SK BETWEEN :low AND :high
begins_with begins_with(SK, :prefix)

begins_with applies only to string sort keys (S). Numeric sort keys (N) do not support begins_with.

Example

var response = await client.QueryAsync(new QueryRequest
{
    TableName = "Orders",
    KeyConditionExpression = "CustomerId = :cid AND OrderId BETWEEN :start AND :end",
    ExpressionAttributeValues = new Dictionary<string, AttributeValue>
    {
        [":cid"] = new() { S = "customer-1" },
        [":start"] = new() { S = "2024-01-01" },
        [":end"] = new() { S = "2024-12-31" }
    },
    ScanIndexForward = false  // newest first
});

foreach (var item in response.Items)
    Console.WriteLine(item["OrderId"].S);

Pagination

QueryResponse? response = null;
do
{
    response = await client.QueryAsync(new QueryRequest
    {
        TableName = "Orders",
        KeyConditionExpression = "CustomerId = :cid",
        ExpressionAttributeValues = new Dictionary<string, AttributeValue>
        {
            [":cid"] = new() { S = "customer-1" }
        },
        Limit = 25,
        ExclusiveStartKey = response?.LastEvaluatedKey
    });

    // Process response.Items...
} while (response.LastEvaluatedKey is { Count: > 0 });

Response fields

Field Description
Items List of matching items (null when Select.COUNT — guard with a null check)
Count Number of items after filtering
ScannedCount Number of items evaluated before filtering
LastEvaluatedKey Key to resume pagination (null if no more results)

Numeric sort key ordering

Sort keys with ScalarAttributeType.N (number) are ordered numerically, not lexicographically. For example, 2 comes before 10, matching DynamoDB behavior. This is achieved via a separate sk_num column in SQLite.

Select.COUNT

When Select = Select.COUNT, the response contains Count and ScannedCount but Items is null (not an empty list — guard with a null check before iterating):

var response = await client.QueryAsync(new QueryRequest
{
    TableName = "Orders",
    KeyConditionExpression = "CustomerId = :cid",
    ExpressionAttributeValues = new Dictionary<string, AttributeValue>
    {
        [":cid"] = new() { S = "customer-1" }
    },
    Select = Select.COUNT
});
Console.WriteLine($"Total: {response.Count}");

ScanAsync

Reads every item in a table or index, with optional filtering.

Signatures

Task<ScanResponse> ScanAsync(ScanRequest request, CancellationToken ct = default)

// Legacy overloads
Task<ScanResponse> ScanAsync(string tableName, List<string> attributesToGet, CancellationToken ct = default)
Task<ScanResponse> ScanAsync(string tableName, Dictionary<string, Condition> scanFilter, CancellationToken ct = default)
Task<ScanResponse> ScanAsync(string tableName, List<string> attributesToGet, Dictionary<string, Condition> scanFilter, CancellationToken ct = default)

Parameters

  • TableName (required) — target table
  • FilterExpression — condition to filter results
  • ProjectionExpression — attributes to return
  • IndexName — scan a secondary index
  • Limit — maximum items to evaluate
  • ExclusiveStartKey — resume pagination
  • SelectALL_ATTRIBUTES, COUNT, or ALL_PROJECTED_ATTRIBUTES
  • ExpressionAttributeNames / ExpressionAttributeValues — expression substitutions
  • ConsistentRead — accepted but has no effect (always consistent)

Example

var response = await client.ScanAsync(new ScanRequest
{
    TableName = "Users",
    FilterExpression = "Age > :minAge",
    ExpressionAttributeValues = new Dictionary<string, AttributeValue>
    {
        [":minAge"] = new() { N = "21" }
    },
    ProjectionExpression = "UserId, #n, Age",
    ExpressionAttributeNames = new Dictionary<string, string>
    {
        ["#n"] = "Name"
    }
});

Pagination

Scan pagination works identically to Query — use Limit and ExclusiveStartKey:

ScanResponse? response = null;
do
{
    response = await client.ScanAsync(new ScanRequest
    {
        TableName = "Users",
        Limit = 100,
        ExclusiveStartKey = response?.LastEvaluatedKey
    });

    // Process response.Items...
} while (response.LastEvaluatedKey is { Count: > 0 });

Querying secondary indexes

Pass IndexName to query or scan a secondary index:

var response = await client.QueryAsync(new QueryRequest
{
    TableName = "Orders",
    IndexName = "StatusIndex",
    KeyConditionExpression = "#s = :status",
    ExpressionAttributeNames = new Dictionary<string, string> { ["#s"] = "Status" },
    ExpressionAttributeValues = new Dictionary<string, AttributeValue>
    {
        [":status"] = new() { S = "shipped" }
    }
});

See Secondary Indexes for details on index projection and behavior.

Legacy API support

DynamoDbLite supports the legacy KeyConditions, QueryFilter, and ScanFilter dictionary-based APIs. These are automatically converted to expression-based equivalents internally.

Supported legacy comparison operators

For QueryFilter and ScanFilter: EQ, NE, LT, LE, GT, GE, BEGINS_WITH, CONTAINS, BETWEEN, NOT_NULL, NULL.

For KeyConditions only EQ, LT, LE, GT, GE, BEGINS_WITH, and BETWEEN are valid (matching DynamoDB constraints — NE and the rest are filter-only operators).

Parity notes

  • ConsistentRead is accepted but all reads are strongly consistent (SQLite behavior)
  • ConsistentRead = true on a GSI query throws AmazonDynamoDBException — matches DynamoDB, which does not support consistent reads on global secondary indexes
  • Parallel scan: TotalSegments / Segment partition results by FNV-1a hash of the partition key — each segment returns disjoint items. Segmentation is applied post-retrieval, so it does not reduce per-call work, and Limit applies to the pre-segmentation row count (a call with Limit=100 and TotalSegments=4 returns roughly 25 items per segment, not 100).
  • Limit applies before FilterExpression (matches DynamoDB behavior)
  • TTL-expired items are automatically filtered out of results
  • LastEvaluatedKey is returned whenever the number of evaluated items equals Limit, including on the final page. Loop callers should expect an empty Items on the next call when this happens.

Clone this wiki locally