Which one is faster/better option to obtain last possible Id in EF: MaxAsync vs LastAsync?

Viewed 227

I have a table that has no auto increment on Id column. While creating record i need to set Id, but problem occurs how do I get possible Id for record that's new.

First attempt i did was:

var ids = await db.Table.Select(e => e.Id).ToListAsync();
var id = Enumerable.Range(1, int.MaxValue).Except(ids).First();

This seems too heavy and there should be better way. Second attempt:

var id = await db.Table.MaxAsync(e => e.Id);

But then I wondering what MaxAsync is different from LastAsync.

From Microsoft documentation I see that MaxAsync

Asynchronously returns the maximum value of a sequence.

Meanwhile this Microsoft documentation of LastAsync

Asynchronously returns the last element of a sequence that satisfies a specified condition.

Whats the difference and which one of these two are better to use in order to get the last possible Id.

3 Answers

"Faster" you'd have to determine by measuring, as there are a number of factors that could go into that. Perhaps as part of your research you could profile the target database and observe the queries that EF is sending to it and see which query you prefer.

"Better" is a somewhat different question here. For starters, one major difference to note is that MaxAsync will always return the maximum value for that column, but LastAsync makes no such guarantee. It will return the last record in the results, but what is that last record? It's probably going to have the highest ID in your setup, but that's not guaranteed. Because the sort order of results from SQL is not guaranteed without specifying a sort. For example:

var id = await db.Table.OrderBy(e => e.Id).Select(e => e.Id).LastAsync();

Or it may be "faster" to sort descending and take only the first record:

var id = await db.Table.OrderByDescending(e => e.Id).Select(e => e.Id).FirstAsync();

(This is another case where "faster" would be interesting for you to measure. Even throwing in a .Take(1) on that chain of operations could make a difference.)

But what makes "better" more interesting here is that this overall setup is essentially a race condition waiting to happen. Will more than one user ever be using this system simultaneously? In the time between fetching the "last" ID and storing the new record, will another user perform the same operation? It's unlikely, but not guaranteed. Basically it's one of those situations where it'll never happen... until it happens.

Ideally the solution here would be to update the database structure to use its own internally-generated IDs (auto-increment, identity, different RDBMSs have different terminology). If that's not an option for whatever reason, then updating the database structure to use a GUID is a common viable option for client-generated IDs.

Failing either of those, you find yourself in a potential race condition situation and you want to narrow that window as much as possible. Perhaps by creating a stored procedure which performs this operation, so the client system still only invokes one DB operation and within that stored procedure you'd have an INSERT with a nested SELECT that fetches/increments the ID. Not ideal, but could work.

I wouldn't advise you to use any of this functions to obtain a new id. If you have one (and only one) user in your db, it will be ok to use them (I think, LastAsync will be faster). But what will happen if two users will add new records simultaneously? They will obtain the same id value!

So consider to use db built-in features for new id generating. You should check the db documentation what is it - for example, MS SQL uses sequences, Firebird - generators and so on. You can use BEFORE INSERT triggers to obtain new id value on the db-side.

The difference is that MaxAsync returns the largest value in the sequence, while LastAsync returns the final value in the sequence, as you stated in your question. If your table's IDs are guaranteed to be in ascending order, incremented by 1 each time, then the result of these two will always be the same, although I suspect LastAsync will be faster because all it needs to do is find the length of the collection and access the value at that index, whereas MaxAsync must either iterate through the collection to find the largest value within it.

As for what is the better solution, that depends how certain you are that the IDs have been consistently incremented throughout the lifetime of the database. If you know they have always been incremented by 1 for every new record, I see no problem using LastAsync. If you do not, MaxAsync will ensure you can always produce a unique ID 1 larger than the previous largest, at the cost of some running time.

Related