Skip to content

Querying

Basic Query

# Query by partition key — returns all items with that key
orders = Order.query("customer-456").all()

# First result only
first_order = Order.query("customer-456").first()

# Limit results
recent_orders = Order.query("customer-456").limit(10).all()

Sort Key Conditions

# Exact match
order = Order.query("customer-456").eq("order-789").first()

# Prefix match
posts_2023 = BlogPost.query("author-123").starts_with("2023-").all()

# Range queries
posts = BlogPost.query("author-123").between(
    datetime(2023, 1, 1, tzinfo=timezone.utc),
    datetime(2023, 12, 31, tzinfo=timezone.utc)
).all()

# Comparisons
expensive = Product.query("category").gt(100.0).all()
cheap = Product.query("category").lt(10.0).all()

Available sort key operators: eq(), starts_with(), between(), gt(), lt(), ge(), le()

GSI Queries

# Query by GSI partition key
tech_posts = BlogPost.query_index("CategoryIndex", "technology").all()

# With sort key condition
recent_tech = BlogPost.query_index("CategoryIndex", "technology") \
    .starts_with("2024-") \
    .limit(20) \
    .all()

Filtering (Non-Key Attributes)

Filters apply after DynamoDB retrieves items. Use key conditions whenever possible for better performance.

from dynantic import Attr

# Single filter
high_rated = Movie.query(2013).filter(Attr("rating") >= 8.0).all()

# Multiple filters (combined with AND)
popular_dramas = (
    Movie.query(2013)
    .filter(Attr("rating") >= 8.0)
    .filter(Attr("genres").contains("Drama"))
    .all()
)

# Complex conditions (OR, AND, NOT)
condition = (Attr("rating") >= 8.0) | Attr("genres").contains("Sci-Fi")
movies = Movie.query(2013).filter(condition).all()

# Filter with key condition
results = (
    Movie.query(2013)
    .starts_with("Inter")
    .filter(Attr("rating") < 8.5)
    .all()
)

Performance

Filters are applied after DynamoDB retrieves items, so you still pay for the read capacity of all scanned items. Use key conditions whenever possible.

Filter Operators

Operator Example
==, !=, <, <=, >, >= Attr("rating") >= 8.0
.contains() Attr("genres").contains("Drama")
.begins_with() Attr("title").begins_with("I")
.exists() Attr("email").exists()
.not_exists() Attr("deleted_at").not_exists()
.between() Attr("rating").between(7.0, 9.0)
.is_in() Attr("status").is_in(["active", "pending"])

Logical Operators

Operator Meaning Example
& AND (Attr("a") > 1) & (Attr("b") < 10)
\| OR (Attr("a") > 1) \| (Attr("b") < 10)
~ NOT ~Attr("deleted").exists()

Metaclass DSL vs Attr()

Dynantic provides a metaclass-based DSL that lets you use model fields directly in filter expressions:

# Metaclass DSL — concise, works at runtime
high_rated = Movie.query(2013).filter(Movie.rating >= 8.0).all()

# Attr() — explicit, mypy-compatible
high_rated = Movie.query(2013).filter(Attr("rating") >= 8.0).all()

Use Attr() in production code

Mypy doesn't understand the metaclass DSL (Movie.rating >= 8.0) because class attributes are resolved dynamically at runtime. Use Attr("field_name") for type-safe code.

Scanning

Scans read the entire table (or index). Use sparingly.

# Scan all items
for user in User.scan():
    print(user.email)

# Scan with limit
for user in User.scan(limit=100):
    process(user)

# Scan with filter
active_users = User.scan().filter(Attr("status") == "active").all()

# Scan GSI
for order in Order.scan(index_name="status-index"):
    print(order.status)

Cost

Scans consume read capacity proportional to the total table size, not the number of results returned. Prefer queries over scans.