Query patterns
This guide explains how queries are executed today in CloudStorageORM, which patterns are optimized, and which patterns fall back to in-memory evaluation.
How query execution works (current behavior)
CloudStorageORM currently uses two execution paths:
- Primary-key optimized path
- Used when the predicate is recognized as a constraint on the entity primary key (
==,>,>=,<,<=).
- Used when the predicate is recognized as a constraint on the entity primary key (
- In-memory path
- Used for non-key predicates or predicates that cannot be translated into a primary-key constraint.
- CloudStorageORM loads entity objects from storage and applies LINQ operators in memory.
In practice, this means your assumption is correct: for non-key filtering, CloudStorageORM may need to enumerate and materialize many objects before filtering.
Basic queries
Get all entities
var allUsers = await context.Users.ToListAsync();
Filter by condition
var activeUsers = await context.Users
.Where(u => u.Status == "Active")
.ToListAsync();
Statusis a non-key property, so this query is evaluated in memory after loading entities.
Primary-key optimized queries
Query by primary key
CloudStorageORM optimizes direct primary-key equality lookups:
// Efficient: direct range-aware load
var user = await context.Users
.FirstOrDefaultAsync(u => u.Id == "123");
Range queries
CloudStorageORM recognizes range constraints on the primary key using comparison operators:
// Load users with IDs > "100" and < "200"
var users = await context.Users
.Where(u => u.Id > "100" && u.Id < "200")
.ToListAsync();
// Supported operators: >, >=, <, <=
Range queries are more efficient than full scans, but still depend on object listing. They are not equivalent to relational index seek/scan behavior.
Single entity retrieval
// Using FirstOrDefault (efficient for PK)
var user = await context.Users
.FirstOrDefaultAsync(u => u.Id == "123");
// Using Single (throws if not found)
var user = await context.Users
.SingleAsync(u => u.Id == "123");
Non-key predicates (in-memory)
Predicates on non-key fields are currently evaluated in memory:
var premiumUsers = await context.Users
.Where(u => u.Plan == "Premium")
.ToListAsync();
This pattern can require loading all objects for the entity type before applying the predicate.
Practical guidance
- Prefer primary-key equality for hot-path reads.
- Prefer primary-key ranges over broad non-key filtering when possible.
- For frequent non-key access patterns, maintain a separate lookup/projection model designed for key-based reads.
- Use feature flags and incremental rollouts when moving relational query workloads to object storage.
Supported LINQ operations
| Operation | Supported | Optimized path | Notes |
|---|---|---|---|
Where() |
✅ | ⚠️ key-only | Primary-key predicates can be optimized; non-key predicates run in memory |
FirstOrDefault() |
✅ | ⚠️ key-only | Optimized for recognized primary-key predicates |
Single() |
✅ | ⚠️ key-only | Optimized for recognized primary-key predicates |
Any() |
✅ | ⚠️ key-only | Non-key checks may require materialization |
Count() |
✅ | ⚠️ key-only | Non-key counts may require materialization |
ToList() |
✅ | N/A | Materializes query results |
ToListAsync() |
✅ | N/A | Async materialization |
Select() |
✅ | ⚠️ key-only | Projection is applied after data is loaded |
Pagination
CloudStorageORM now supports Skip/Take pushdown for supported query shapes. When pushdown applies, object paths are
paged provider-side and only the requested slice is materialized.
Pushdown applies when:
- pagination appears as a
Skip(...).Take(...)/Take(...).Skip(...)chain at the query edge - the pre-pagination query shape avoids unsupported operators (
OrderBy*,ThenBy*,Select*,Reverse,GroupBy,Distinct) - any predicate is either absent or recognized as a primary-key constraint (
==,>,>=,<,<=)
Example (eligible for pushdown):
var pageSize = 10;
var pageNumber = 2;
var pagedUsers = await context.Users
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
Example with primary-key range + pagination (eligible for pushdown):
var users = await context.Users
.Where(u => u.Id >= "100" && u.Id < "200")
.Skip(20)
.Take(20)
.ToListAsync();
When the shape is not eligible, CloudStorageORM falls back to materialize-then-slice behavior.
Performance considerations
- Prefer primary-key filters (
Id == ..., key ranges) in latency-sensitive paths - Treat non-key predicates as scan-like in current implementation
- Use async methods (
ToListAsync,FirstOrDefaultAsync) to avoid blocking - Avoid repeated full materialization of the same entity set in request hot paths
Limitations
- No server-side relational query execution; non-key filtering is in-memory
Skip()/Take()pushdown is shape-dependent; unsupported query shapes fall back to in-memory slicing- No
Include()for related entities (object storage is not relational) - Composite-key optimization behavior is limited; optimize primarily for single primary-key constraints
- Complex nested queries may require manual materialization and reshaping