Opened 5 years ago

Closed 4 years ago

#30375 closed Cleanup/optimization (fixed)

Use "NO KEY" when doing select_for_update for PostgreSQL

Reported by: Manuel Weitzman Owned by: Manuel Weitzman
Component: Database layer (models, ORM) Version: dev
Severity: Normal Keywords: postgres, lock, database, operation
Cc: Triage Stage: Ready for checkin
Has patch: yes Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description (last modified by Manuel Weitzman)

Currently Django compiles select for update into one of

  • FOR UPDATE
  • FOR UPDATE NOWAIT
  • FOR UPDATE SKIP LOCKED

All of these acquire a lock which conflicts with KEY SHARE locks. This means that updates on tables which references a row locked by FOR UPDATE will have to wait until the lock is released. This is to avoid any unexpected changes on PRIMARY KEY fields but, in Django, primary key fields are read-only, so locking them makes no sense.

There is no need to acquire these kind of locks, instead, we should (at least be able to) use

  • FOR NO KEY UPDATE
  • FOR NO KEY UPDATE NOWAIT
  • FOR NO KEY UPDATE SKIP LOCKED

which can be easily done by overriding BaseOperation.for_update_sql in Postgres DatabaseOperation

For 1.11

def for_update_sql(self, nowait=False, skip_locked=False):
    """
    Returns the FOR UPDATE SQL clause to lock rows for an update operation.
    """
    if nowait:
        return 'FOR NO KEY UPDATE NOWAIT'
    elif skip_locked:
        return 'FOR NO KEY UPDATE SKIP LOCKED'
    else:
        return 'FOR NO KEY UPDATE'

For 2.2

def for_update_sql(self, nowait=False, skip_locked=False, of=()):
    """
    Return the FOR UPDATE SQL clause to lock rows for an update operation.
    """
    return 'FOR NO KEY UPDATE%s%s%s' % (
        ' OF %s' % ', '.join(of) if of else '',
        ' NOWAIT' if nowait else '',
        ' SKIP LOCKED' if skip_locked else '',
    )

Not doing so causes lock contention in our use case.

Change History (9)

comment:1 by Manuel Weitzman, 5 years ago

Description: modified (diff)
Version: 1.112.2

comment:2 by Simon Charette, 5 years ago

Version: 2.2master

... in Django, primary key fields are read-only, so locking them makes no sense

That's not true AFAIK. Even if AutoField are backed by sequences and generated on the database side they can still be overridden by developers. Same thing with non-AutoField primary keys (e.g. UUIDField(default=uuid.uuid4)).

On the other hand updating a primary key while holding a lock is really uncommon so I wouldn't be against adding a select_for_udate(primary_key) parameter that defaults to False. We just have to make sure that we still allow these operations to be performed somehow.

in reply to:  2 comment:3 by Manuel Weitzman, 5 years ago

Replying to Simon Charette:

... in Django, primary key fields are read-only, so locking them makes no sense

That's not true AFAIK. Even if AutoField are backed by sequences and generated on the database side they can still be overridden by developers. Same thing with non-AutoField primary keys (e.g. UUIDField(default=uuid.uuid4)).

Then the docs are wrong or at least misleading: "The primary key field is read-only. If you change the value of the primary key on an existing object and then save it, a new object will be created alongside the old one."" (https://docs.djangoproject.com/en/2.2/topics/db/models/)

Which means that UPDATEs over PKs are transformed into INSERTs

On the other hand updating a primary key while holding a lock is really uncommon so I wouldn't be against adding a select_for_udate(primary_key) parameter that defaults to False. We just have to make sure that we still allow these operations to be performed somehow.

That is possible, I tested today and only a few changes are required to achieve this behavior. But if UPDATEs are converted to INSERTs, then NO KEY should be used by default when using a PostgreSQL backend, shouldn't it?
Maybe I could send a PR soon for review, I think I can program an approach which uses an extra parameter!

Last edited 5 years ago by Manuel Weitzman (previous) (diff)

comment:4 by Simon Charette, 5 years ago

The docs are correct for save but primary keys are still updatable through queryset.filter(pk=old_value).update(pk=new_value).

Feel free to submit a PR. It'd be great to investigate if other backends have a similar option as well.

comment:5 by Carlton Gibson, 5 years ago

Triage Stage: UnreviewedAccepted

comment:6 by Mariusz Felisiak, 5 years ago

Needs documentation: set
Owner: changed from nobody to Manuel Weitzman
Patch needs improvement: set
Status: newassigned

comment:7 by Mads Jensen, 4 years ago

Needs documentation: unset
Patch needs improvement: unset

This needs to be in the review queue :)

comment:8 by Mariusz Felisiak, 4 years ago

Triage Stage: AcceptedReady for checkin

comment:9 by Mariusz Felisiak <felisiak.mariusz@…>, 4 years ago

Resolution: fixed
Status: assignedclosed

In a4e6030:

Fixed #30375 -- Added FOR NO KEY UPDATE support to QuerySet.select_for_update() on PostgreSQL.

Note: See TracTickets for help on using tickets.
Back to Top