After reading and watching infinite videos on Cosmos Db partitioning, I sweared only on point reads.
In the NoSQL .Net SDK this translates to using ReadItemAsync<T>
. But it seems I found a cheaper method and would like to know why that is expected, if it is expected.
Testing on the emulator, I have a container with a partition key path set to /userId
. I have 200 documents representing a User
poco. Each User
is of the form :
public class User
{
public User(string userId)
{
UserId = userId;
}
public required string Id { get; set; }
public string UserId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string FullName { get; set; }
public string UserName { get; set; }
public string Email { get; set; }
public IList<Order> Orders { get; set; }
public IList<Sale<Order, User>> Sales { get; set; }
public string Type { get; } = nameof(User);
}
Importantly for what follows, each user has an average of 500 orders and 500 sales. I understand that I have to keep the documents lightweight, but I am testing. This results in an average document size of circa 120kB, so I believe this is still ok (<<2 MB).
More specifically, the app is designed such that I expect customers to do:
User
object (i.e. the complete document in the container)User
.Thus, I have in addition the class:
public class UserViewModel
{
public string FirstName { get; set; }
}
which essentially represents a view of the User
class without the Orders and Sales and I focus only on the first name for testing.
Question 1
If I am interested only in the View Model to display it to the user, how I can ask Cosmos db not to return the entire User
object to the web app (Asp.Net Core) but rather just do a selection on the cosmos db server and send me the View Model?
Test 1:
Listen to the Gods of Cosmos Db and do a point read by using as a Template the UserViewModel
, i.e. do this
var user = await _container.ReadItemAsync<UserViewModel>("fbea1444-8dcb-cb89-8cde-7adf8a580812", new PartitionKey("920a8e38-481a-3874-f14e-6de85029419f"));
The result of this point read on the logical partition gives
"resource": {
"firstName": "Reggie"
},
"statusCode": 200,
"diagnostics": { },
"requestCharge": 4.76
and if, instead, I cast to the User
var user = await _container.ReadItemAsync<User>("fbea1444-8dcb-cb89-8cde-7adf8a580812", new PartitionKey("920a8e38-481a-3874-f14e-6de85029419f"));
I obtain of course the (huge) User
as a resource, but the request charge is the same, 4.76 RUs.
Question 2: That the RU is the same, does this mean that Cosmos Db sends me back the entire object even if I use the first option, i.e. UserViewModel
, and it is my web app which does the cast?
Test 2: Where Clause on the partition userId
In the hope of getting better, I abandoned the point read to use instead GetItemLinqQueryable
// Get LINQ IQueryable object
var queryable = _container.GetItemLinqQueryable<User>();
// Construct LINQ query
var matches = queryable
.Where(b => b.UserId == "920a8e38-481a-3874-f14e-6de85029419f")
.Select(u => new UserViewModel() { FirstName = u.FirstName });
// Convert to feed iterator
using var linqFeed = matches.ToFeedIterator();
var totalCharge = 0.0;
// Iterate query result pages
while (linqFeed.HasMoreResults)
{
var response = await linqFeed.ReadNextAsync();
result.Add($"Request Charge {response.RequestCharge} {totalCharge += response.RequestCharge} RUs");
result.Add(response);
}
"Request Charge 3.03 3.03 RUs",
[
{
"firstName": "Reggie"
}
]
Question 3
Why this method (3.03 RUs) is more efficient than a point read (4.76 Rus)?
Test 3 Where Clause on FirstName (not the parition key)
From the above code, I also tested by using the Where clause on something else than the parition key property UserId
, i.e.
var matches = queryable
.Where(b => b.FirstName == "Reggie")
.Select(u => new UserViewModel() { FirstName = u.FirstName });
Question 4
The Request charge is still 3.03 Ru. Why is that?
Test 4 Where clause with no Select
// Get LINQ IQueryable object
var queryable = _container.GetItemLinqQueryable<UserViewModel>();
// Construct LINQ query
var matches = queryable
.Where(b => b.FirstName == "Reggie");
Question 5
The Request charge is now 3.8 RUs. Why is it higher than the 3.03RUs?
Adding a Select clause will allow me to get 3.03Rus, whereas it does not make sense to have the Select clause given that I already use UserViewModel
in GetItemLinqQueryable
.
Note that I also tested by adding a UserId property to the ViewModel, thinking this is partition issue, and thus replaced the above where clause by .Where(b=>b.UserId == "920a8e38-481a-3874-f14e-6de85029419f")
. But the request charge is the same, 3.8 Rus > 3.03 Rus.
These questions will allow me to understand how I can best ask the Cosmos Db servers to the job of sending me only the bits I need. My first test 1 above failed, and it seems the reason being that ReadItemAsync<T>
does not ask Cosmos Db to "cast" on its server to send the much smaller ViewModel
along the wire. Instead, what this method does, I believe, tests, is that it merely gets the entire document from Cosmos Db, and does the cast on my own app server.
Thank you
I obtain of course the (huge) User as a resource, but the request charge is the same, 4.76 RUs
The Model (type) is only used for deserialization, ReadItem will always return the entire document. If you have a Model that has less properties, the others will simply not get deserialized, but are part of the response.
Why this method (3.03 RUs) is more efficient than a point read (4.76 Rus)?
Probably because the data volume is less, it's not a fair comparison because you are doing a projection on the query, thus reducing the volume of data that is returned from the service.
The Request charge is now 3.8 RUs. Why is it higher than the 3.03RUs?
Your query is different, it is not filtering and thus processing and returning more data on the service.
Bottom line is, a ReadItem will be better than a SELECT * WHERE Id/PartitionKey, but if the documents are big, a SELECT <single property>
WHERE Id/PartitionKey where the projection leaves out the big data properties, it might be cheaper because the volume of data is much lower.