Optimization of Permission Queries in User Add Form on Django Admin Panel

When creating a user through the Django admin panel, the form is significantly slow, executing over 500 queries. This performance issue arises because permissions are not pre-fetched in the user creation form, leading to a high number of database queries.

Steps to Reproduce

  1. Add permissions in fieldset
  2. Navigate to the Django admin panel.
  3. Go to the “Users” section.
  4. Click “Add User” to open the user creation form.
  5. Notice the slowness and inspect the number of queries executed.

Observed Behavior

The user creation form in the Django admin panel is extremely slow due to the execution of over 500 queries related to fetching permissions.

Expected Behavior

The user creation form should load efficiently with minimal queries by prefetching related permissions, thus improving performance and user experience.

Proposed Solution

To address this issue, I suggest prefetching related permission objects when rendering the user creation form (if permissions are added in fieldset). By doing this, the number of queries can be drastically reduced, leading to a significant improvement in form load times. I will be happy to provide the solution :slight_smile:

Welcome @Alihassanc5 !

I am unable to recreate or to verify this report. I don’t see anything like this behavior in my test lab.

Are you working with a custom User model? Are you using a custom ModelAdmin class or form for this model? Have you modified the permissions system? Basically, what is different between your system and base Django?

What versions of Django and Python are you using? What database engine are you using?

What are the specific queries being issued here?

The more details that you provide concerning your environment, the more likely we will be able to help identify the root issue.

So I created my custom UserAdmin which is inherited from Base UserAdmin class, I am using default Django model.
Python: 3.10.14
Django: 4.2.6
DB Engine: Postgres

I have added user_permissions in add_fieldsets:

add_fieldsets = (
        (
            None,
            {
                "classes": ("wide",),
                "fields": (
                    "first_name",
                    "last_name",
                    "email",
                    "password1",
                    "password2",
                    "is_active",
                    "is_staff",
                    "is_superuser",
                    "user_permissions",
                ),
            },
        ),
    )

user_permissions causing an issue because it’s fetching all the related content-type in a separate query.

So, my proposed solutions is to fetch all the related permissions if it’s in fieldset, same like we are doing in our UserChangeForm

class UserChangeForm(forms.ModelForm):
    password = ReadOnlyPasswordHashField(
        label=_("Password"),
        help_text=_(
            "Raw passwords are not stored, so there is no way to see this "
            "user’s password, but you can change the password using "
            '<a href="{}">this form</a>.'
        ),
    )

    class Meta:
        model = User
        fields = "__all__"
        field_classes = {"username": UsernameField}

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        password = self.fields.get("password")
        if password:
            password.help_text = password.help_text.format(
                f"../../{self.instance.pk}/password/"
            )
        user_permissions = self.fields.get("user_permissions")
        if user_permissions:
            user_permissions.queryset = user_permissions.queryset.select_related(
                "content_type"
            )
1 Like

@KenWhitesell what’s your thoughts on it? Could you please assign this issue to me?

Personally, I want to do some research - dig into the mailing list, trac, and the git history to see if there’s a documented reason why the admin is currently set up with this “2-step” process, with a minimal “create” page and everything else on the “edit” page.

It also seems to me that this is a feature that can be added as an external package - which is (in general terms) how additions to Django itself can be tested without committing them directly to core.

(And, if there is a specific reason why the existing admin is constructed this way, then I would suggest that a third-party package is the best way to move forward.)

The ticket needs to be “triaged” by one of the Fellows.

If you haven’t already done so, I suggest you read Reporting bugs and requesting features | Django documentation | Django and the related page at Triaging tickets | Django documentation | Django to understand the process that is followed.

1 Like