Skip to content

Support SELECT FOR UPDATE / UPDLOCK (pessimistic concurrency) #26042

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
Tracked by #14496
Lobosque opened this issue Sep 15, 2021 · 14 comments
Open
Tracked by #14496

Support SELECT FOR UPDATE / UPDLOCK (pessimistic concurrency) #26042

Lobosque opened this issue Sep 15, 2021 · 14 comments

Comments

@Lobosque
Copy link

I want to garantee that only one Api Request (Database Transaction) can modify an entity at a given time. this could be done by selecting a row with "SELECT FOR UPDATE" inside a transaction.

Something like:

context.Books
                        .Where(b => b.Id == 1)
                        .SelectForUpdate(skipLocked: true)
                        .FirstOrDefault();

Would generate something like:

                SELECT *
                FROM books b
                WHERE b.id = 1
                FOR UPDATE SKIP LOCKED
                LIMIT 1";

I think that EF itself does not need to track at any level that the entity was selected with a table hint, it only need to be able to express the SQL.

@roji
Copy link
Member

roji commented Sep 15, 2021

This may be covered by #6717, and may be achievable today by using raw SQL and composing your LINQ operators over it (see the docs), assuming SQL Server allows SELECT FOR UPDATE in a subquery.

@ajcvickers ajcvickers changed the title Get an entity with select for update Get an entity with select for update (Pessimistic concurrency) Sep 17, 2021
@ajcvickers ajcvickers added this to the Backlog milestone Sep 17, 2021
@roji
Copy link
Member

roji commented Dec 1, 2021

Note: use table annotations to represent this (#26858).

@roji
Copy link
Member

roji commented Dec 14, 2021

Cross-database investigation on SELECT FOR UPDATE

The FOR UPDATE clause makes the database lock rows which are selected by the query; other transactions which query a locked row with FOR UPDATE (or UPDATE it) will wait until the first transaction ends.

Comparison to optimistic concurrency:

  • This removes the need to handle optimistic concurrency exceptions and retry, simplifying user code significantly.
  • No need for a concurrency token, and no need to select it back when updating.
  • If two transactions select rows in reverse order, a deadlock occurs, causing an error. This is similar to the deadlock that can happen with UPDATE (docs). This can be mitigated by using the same ordering, or if not feasible, catching the deadlock exception and retrying.
  • If transactions are long (not recommended), then competing transactions may wait for a long time. With optimistic concurrency this does not occur.
  • In disconnected scenarios, selection and update typically cannot occur in the same transaction. Unless the database row is selected a second time just before update, SELECT FOR UPDATE isn't relevant.

PostgreSQL

Documentation

DROP TABLE IF EXISTS foo;
CREATE TABLE foo (bar INT);
INSERT INTO foo VALUES (1), (2), (3), (4), (5);
CREATE TABLE foo2 (bar INT);
INSERT INTO foo2 VALUES (1), (2), (3), (4), (5);

-- Simple, top-level scenario
SELECT * FROM foo FOR UPDATE;

-- On a specific table with JOIN:
SELECT * FROM foo
JOIN foo2 ON foo2.bar = foo.bar FOR UPDATE;

-- Inside a subquery:
SELECT * FROM (
    SELECT * FROM foo FOR UPDATE
) x;
  • Instead of blocking on locked rows, you can specify NOWAIT (to get an error) or SKIP LOCKED (to skip locked rows altogether).
  • Weaker lock strengths are available, e.g. SELECT ... FOR SHARE which blocks updates but allows other FOR SHARE locks (read-write lock).

SQL Server

No SELECT FOR UPDATE syntax - the UPDLOCK table hint is used instead (documentation);

CREATE TABLE foo (bar INT);
INSERT INTO foo VALUES (1), (2), (3), (4), (5);
CREATE TABLE foo2 (bar INT);
INSERT INTO foo2 VALUES (1), (2), (3), (4), (5);

-- Simple, top-level scenario
SELECT * FROM foo WITH (UPDLOCK);

-- On a specific table with JOIN:
SELECT * FROM foo
JOIN foo2 WITH (UPDLOCK) ON foo2.bar = foo.bar;

-- Inside a subquery:
SELECT * FROM (
    SELECT * FROM foo (UPDLOCK)
) x;

MariaDB

MariaDB documentation
MySQL documentation

SQL: identical to PostgreSQL above

  • Also supports IN SHARE MODE for read/write locking, like PostgreSQL FOR SHARE.

SQLite

Not supported (no concurrency/row locking)

Oracle

Documentation

Firebird

Documentation (some interaction with WITH LOCK feature)


Based on the above, we could consider doing the following:

  • Building on table annotations, we could introduce a relational-level ForUpdate().
  • This would be generated as FOR UPDATE by default, with SQL Server overriding to generate UPDLOCK instead.
  • For Include, the metadata extension could be specified inside the Include (i.e. context.Blogs.Include(b => b.Posts).ForUpdate()). This may require additional work in Query: improve TableExpressionBase extensibility by adding annotations #26858.
  • PostgreSQL, MariaDB would have an additional ForShare() and other operators.

/cc @maumar

@roji roji removed this from the Backlog milestone Dec 14, 2021
@roji roji changed the title Get an entity with select for update (Pessimistic concurrency) Support SELECT FOR UPDATE / UPDLOCK (pessimistic concurrency) Dec 14, 2021
@ajcvickers ajcvickers added this to the Backlog milestone Jan 5, 2022
@benlongo
Copy link

Another use case is FOR UPDATE SKIP LOCKED to implement a queue in postgres.

@n0099
Copy link

n0099 commented Feb 8, 2023

For anyone who can't wait for this feature, check out this simple command interceptor or try Linq2Db.

@roji
Copy link
Member

roji commented Apr 24, 2023

Another use-case: atomically update something with ExecuteUpdate. This can be done today with optimistic locking, but that requires retrying when the update fails; that means some sort of random delay + backoff strategy is needed, where pessimistic locking can solve this much more easily.

@neistow

This comment has been minimized.

@roji

This comment has been minimized.

@nimanikoo
Copy link

Hello everyone,

I’ve reviewed this issue and would like to contribute by implementing support for SELECT FOR UPDATE in EF Core. This functionality is critical for scenarios requiring pessimistic concurrency control, ensuring that rows are locked for updates and preventing conflicts in multi-transaction environments.

Proposed Approach:

  1. New LINQ Method: Introduce a method like SelectForUpdate() in LINQ queries to enable users to specify locking hints.
    csharp
    var entity = context.MyEntities
    .Where(e => e.Id == 1)
    .SelectForUpdate(skipLocked: true) // Example usage
    .FirstOrDefault();

  2. SQL Translation: Modify the SQL generation pipeline to support PostgreSQL’s FOR UPDATE clause and its variations (e.g., FOR UPDATE SKIP LOCKED, FOR UPDATE NOWAIT).

  3. Provider-Specific Behavior: Ensure the feature is implemented in a provider-agnostic way but supports PostgreSQL-specific syntax and extensions where possible. Other providers like SQL Server could use similar locking mechanisms (e.g., UPDLOCK).

Next Steps:

  • I will fork the repository, start implementing this feature, and submit a pull request referencing this issue.
  • Before proceeding, I’d like to confirm if there are any specific guidelines or considerations I should follow for this implementation.

Looking forward to contributing and helping bring this feature to the community! Please let me know if you have any feedback or additional suggestions.

Best regards,
nimanikoo

@RunGuitarMan
Copy link

Hi nimanikoo, thanks for taking the initiative!

Supporting SELECT FOR UPDATE — especially with options like SKIP LOCKED and NOWAIT — would indeed be a great addition for scenarios involving high-concurrency workloads.

Just a few probably obvious PostgreSQL-specific notes that might help with implementation (just in case):

PostgreSQL Considerations
• Clause Positioning:
The FOR UPDATE clause must appear at the end of the SQL statement, after ORDER BY, LIMIT, and OFFSET.
• Support for Variants:
PostgreSQL supports FOR UPDATE, FOR NO KEY UPDATE, FOR SHARE, and FOR KEY SHARE, each with optional modifiers like NOWAIT and SKIP LOCKED.
It would be great if the API allows selecting these modes (e.g., SelectForUpdate(mode: ForUpdateMode.NoKeyUpdate, skipLocked: true)).
• Row-Level Locking Scope:
Locks apply only to selected rows — so we must ensure that .SelectForUpdate() is applied in queries where FirstOrDefault() or ToList() is called, not in projections.
• Compatibility with Batching:
Ensure the locking clause does not interfere with any batching or pagination strategies (e.g., LIMIT + SKIP LOCKED).
• Also, don't forget that the FOR UPDATE SKIP LOCKED lock cannot be applied to a query with aggregates.

Suggestions
• Consider adding a new expression node (e.g., ForUpdateExpression) and ensure that it’s handled in QuerySqlGenerator of the PostgreSQL provider.

Looking forward to seeing your PR!

@roji
Copy link
Member

roji commented May 12, 2025

@nimanikoo I really appreciate the offer to work on this and the enthusiasm; however, it's important to point out that pessimistic locking is quite a big feature which would involve some potentially deep changes in the EF query pipeline - which is complex. If you don't have any prior experience with the query pipeline and/or this is your first (big) contribution to EF, I'd advise trying to work on something simpler first.

More concretely on your proposal:

var entity = context.MyEntities
.Where(e => e.Id == 1)
.SelectForUpdate(skipLocked: true) // Example usage
.FirstOrDefault();

Note that FOR UPDATE/UPDLOCK are table-scoped, and not query-scoped; in other words, different tables in the query can have different pessimistic locking settings. You'd need to think about what that means for your proposed API, and how users express different settings for different tables.

Finally, I'd be curious to hear about why pessimistic locking is necessary in your scenario, and why EF's optimistic concurrency is insufficient. The latter seems to work well enough for the vast majority of cases, which is also why we haven't been prioritizing this.

@nimanikoo
Copy link

hello again @roji ,

Thanks a lot for your detailed response — I really appreciate your insights and the time you took to outline the challenges and considerations involved.

To clarify my motivation and scenario:

I'm currently using EF Core 8 with PostgreSQL. In one of our high-concurrency flows, I need to lock only a single row using SELECT FOR UPDATE, without locking the entire table. This is crucial to prevent concurrent flows from processing the same record at the same time. I want other transactions to wait until the current one finishes — I’m not looking for a table-level lock, but a very targeted row-level lock.

This level of control is essential to ensure correctness in scenarios like payment processing or job scheduling, where race conditions can have serious consequences. Using SELECT FOR UPDATE directly in SQL works for us, but it breaks the composability and maintainability of our LINQ-based code.

While I haven't contributed to EF Core yet, I’d love to get involved. I’ve contributed to large open-source projects such as OpenTelemetry and RedisStack, and I’m confident in my ability to help with complex features — especially with guidance and support from the team.
Of course, I fully realize that implementing a feature like this isn't possible without guidance and support from the team. Your input would be essential at every step, and I’d be glad to work closely with maintainers to get it right.

I completely understand this is a non-trivial change, and I don’t expect to do it in isolation. If this is too large for a first contribution, I’d be very happy to work on smaller issues first to build up context. But if there’s any chance to contribute to this feature — even incrementally or starting with a provider-specific prototype — I’d be excited to collaborate.

Please let me know if there’s any issue you’d recommend as a good starting point. Either way, I’d love to be involved and contribute meaningfully to EF Core.

Thanks again for your time!

Best regards,
nimanikoo

@roji
Copy link
Member

roji commented May 12, 2025

I’m not looking for a table-level lock, but a very targeted row-level lock.

That's very understandable, but my question is more where you feel you need pessimistic locking, as opposed to the optimistic row locking that EF already supports well; the latter is not a table-level lock, but rather a different approach to handling concurrent loads. In some cases, people are simply unaware of optimistic locking (or misunderstand it) and assume that they must use SELECT FOR UPDATE - so I just want to make sure that's not the case here. Optimistic and pessimistic locking have different pros and cons, but where the former can work, it can be more efficient, more compatible with offline workloads, etc.

As to contributing... The team unfortunately has limited capacity at the moment to guide complex, large-scale contributions (which this would be) - I wouldn't want you to do a lot of work, and then for the PR to sit for a long time since we have other priorities. If you're OK with taking this risk, then I'd suggest you take a look at the codebase, and put together a more detailed plan of exactly which types you're proposing to change and how (because your above comment only contains a very high-level overview). I'd also want to know how you plan to handle the case I mentioned above of multiple tables (and subqueries) within the same query.

@neistow
Copy link

neistow commented May 19, 2025

where you feel you need pessimistic locking, as opposed to the optimistic row

My team needed pessimistic locking when implementing a transactional outbox pattern. Optimistic locking won't work and will result in a duplicate event publishing when the app is running in multiple instances.

Also, any scenario that requires calling an external service won't be covered by optimistic locking e.g

var notification = _dbContext.Notification.First();
if(notification.Completed)
{
  return;
}
notification.SetCompleted();

// Nothing prevents two callers from executing this method twice with optimistic concurrency
// even though one of the callers will error out,  we can't unsend the email or undo external operation
_externalService.NotifyWithEmail(notificaton.Id);

_dbContext.SaveChanges();

The above problem is solved by outbox, but in the scenarios where it would be overkill, we don't have an option to simply use pessimistic locking without writing raw SQL.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

8 participants