Opened 6 years ago

Closed 20 months ago

#30256 closed Cleanup/optimization (fixed)

autocomplete_fields cause one or two extra queries for each field wth foreign key or many to many relation

Reported by: George Tantiras Owned by: nobody
Component: contrib.admin Version: dev
Severity: Normal Keywords: autocomplete
Cc: Johannes Maron Triage Stage: Accepted
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

The following model:

models.py

class Child3(models.Model):
    key = models.ForeignKey(Master, on_delete=models.PROTECT)
    child_keys = models.ManyToManyField(Child2)
    boolean = models.BooleanField()

admin.py

@admin.register(models.Child3)
class Child3Admin(admin.ModelAdmin):
    search_fields = ('id', )
    autocomplete_fields = ('key', 'child_keys')
    # form = forms.Child3Form  # Uncomment to enable django-autocomplete-light

When visiting the url of instance with id 1: http://127.0.0.1:8000/admin/inl/child3/1/change/

The queries count varies as follows:

no autocomplete feature: 3 queries
with autocomplete_fields enabled: 6 queries
with django-autocomplete-light enabled: 3 queries

I have not calculated the contentype query that sometimes appears and sometimes not.

When the above model is used as inline to another model the queries for each related instance accumulate.

I have uploaded the autobug app which illustrates the above using 4 related models using foreign keys and many to many fields.

Change History (12)

comment:1 by Carlton Gibson, 6 years ago

Cc: Johannes Maron added
Component: Uncategorizedcontrib.admin
Triage Stage: UnreviewedAccepted
Type: UncategorizedCleanup/optimization

Hi George. Thanks for the report.

Happy to provisionally accept this for an optimization. (I'm reading it as you having a suggestion in mind?)

(@Joe: cc-ing you for comment at this stage: any thoughts?)

comment:2 by Carlton Gibson, 6 years ago

Note: happy to close this as wontfix if there's nothing we can do... IIRC django-autocomplete-light still makes additional HTTP requests to fetch the autocomplete data. (No?)

comment:3 by Johannes Maron, 6 years ago

Hi @George,

thanks for reporting the bug. I am afraid I need a little bit information though. Did you use ~django-debug-toolbar~ by any chance, to identify what the additional queries are?
Based on the code, I don't see where where the additional queries could come from. If you could past the queries here, this would help a lot.

Thanks,
Joe

comment:4 by Carlton Gibson, 6 years ago

Resolution: needsinfo
Status: newclosed
Triage Stage: AcceptedUnreviewed

Thanks Joe! (I'll just move this to needsinfo pending a follow-up.)

comment:5 by George Tantiras, 6 years ago

Hallo, thank you for the response.

It took me some hours to understand why I could not reproduce the above in the project I uploaded, autobug app :

The answer is that the queries are reduced in PostgreSQL and not in SQLite.

I incorporated the debug toolbar in that project and arranged it to use a Postgresql database with those credentials.

I uploaded the queries*.txt files which report all the queries from debug toolbar either with their full traceback or only the queries.

I also uploaded a db.json file with all the database contents in json format.

Here, I paste the queries for the url http://127.0.0.1:8000/admin/inl/child3/1/change/.

The url http://127.0.0.1:8000/admin/inl/master/1/change/ has many more queries because it includes all other models as inlines.

The queries:
Those first 4 queries are common for all three occasions (django-autocomplete, autocomplete-light, no autocomplete at all):

SELECT "django_session"."session_key", "django_session"."session_data", "django_session"."expire_date" FROM "django_session" WHERE ("django_session"."expire_date" > '2019-03-21T15:07:04.528846+00:00'::timestamptz AND "django_session"."session_key" = 'ku98yfv1ublrcwkqfqtguhw5a886241r')

SELECT "auth_user"."id", "auth_user"."password", "auth_user"."last_login", "auth_user"."is_superuser", "auth_user"."username", "auth_user"."first_name", "auth_user"."last_name", "auth_user"."email", "auth_user"."is_staff", "auth_user"."is_active", "auth_user"."date_joined" FROM "auth_user" WHERE "auth_user"."id" = 1

SELECT "inl_child3"."id", "inl_child3"."key_id", "inl_child3"."boolean" FROM "inl_child3" WHERE "inl_child3"."id" = 1

SELECT "inl_child2"."id", "inl_child2"."key_id", "inl_child2"."boolean", "inl_child2"."child_key_id" FROM "inl_child2" INNER JOIN "inl_child3_child_keys" ON ("inl_child2"."id" = "inl_child3_child_keys"."child2_id") WHERE "inl_child3_child_keys"."child3_id" = 1

The following 2 queries are added from django-autocomplete only:

SELECT "inl_master"."id" FROM "inl_master" WHERE "inl_master"."id" IN (1)

SELECT "inl_child2"."id", "inl_child2"."key_id", "inl_child2"."boolean", "inl_child2"."child_key_id" FROM "inl_child2" WHERE "inl_child2"."id" IN (2, 5, 3, 1, 4)

The complete traceback cam be found in this file.

Version 0, edited 6 years ago by George Tantiras (next)

comment:6 by George Tantiras, 6 years ago

Resolution: needsinfo
Status: closednew

comment:7 by Carlton Gibson, 6 years ago

Keywords: autocomplete added
Triage Stage: UnreviewedAccepted
Version: 2.1master

OK, this is a gnarly one but I'm going to Accept it as a possible optimization.

The extra queries are generated when rendering the optgroups for the widgets.

django-autocomplete-light (DAL) excludes unselected self.choices before determining the options to render, i.e. it only initially renders the selected option.

Where the selected option is just the foreign key value that we already selected for the admin this query can be eliminated.

SELECT "inl_child2"."id", "inl_child2"."key_id", "inl_child2"."boolean", "inl_child2"."child_key_id"
FROM "inl_child2" INNER JOIN "inl_child3_child_keys" ON ("inl_child2"."id" = "inl_child3_child_keys"."child2_id") 
WHERE "inl_child3_child_keys"."child3_id" = 1

Looks to already cover in this case:

SELECT "inl_child2"."id", "inl_child2"."key_id", "inl_child2"."boolean", "inl_child2"."child_key_id"
FROM "inl_child2" 
WHERE "inl_child2"."id" IN (2)

(Since the FK points to child2.id = 2 in this case.)

Forcing evaluation of the temporarily reduced queryset (here) causes the query count to match that observed in Django's own autocomplete, since we apply no such optimization. (i.e All choices are required in optgroups() in Django's version.)

Thus in theory there's a gain to be had here.

Thanks for the report George. Fancy working on a patch? 🙂

comment:8 by George Tantiras, 6 years ago

Carlton, thank you for the feedback!

I would like to try for a patch.

comment:9 by George Tantiras, 5 years ago

I cannot reproduce the issue using PostgreSQL-11.5.

Currently, Django Debug Toolbar reports the same number of queries when using dal3, django-autocomplete or "no autocomplete".

However, there is a difference:

  • When using dal3 or "no autocomplete" a query appears (it was not present when I reported this issue):
DECLARE "_django_curs_140333492991744_3" NO SCROLL CURSOR WITH HOLD FOR SELECT ••• FROM "inlines_master" WHERE "inlines_master"."id" IN (1)
  • Django autocomplete reatains its behaviour, it does not produce the above query rather than a proper SELECT as expected (the reason for reporting this issue):
SELECT ••• FROM "inlines_master" WHERE "inlines_master"."id" IN (1)

Is there something right happening or something wrong?

comment:10 by Simon Charette, 5 years ago

The server side cursor (DECLARE CURSOR) get created when using iterator on PostgreSQL; the SQLite backend doesn't support this feature.

The only internal use of .iterator() I can think of is in ModelChoiceIterator.choice (link) if that can be of any help.

comment:11 by Brett Higgins, 5 years ago

I think I'm having the same issue described here, but I'm using the autocomplete functionality built into Django 2. Also, I'm currently on PostgreSQL 9.6.15.

George, does your last comment apply to Django 2 built-in autocomplete? i.e. if we upgrade to a later PostgreSQL, would you expect this issue to go away?

I've also just remembered that we explicitly disable server-side cursors because we use pgbouncer and encountered this issue some time ago: https://code.djangoproject.com/ticket/28062 so I'm not sure if that workaround would work for us (if I'm even understanding it correctly).

comment:12 by George Tantiras, 20 months ago

Resolution: fixed
Status: newclosed

Currently, checking again with Django-4.2 it seems that everything works as expected to work.

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