From f8fa690de3b44883a1bb7f76773f3fe2792713fb Mon Sep 17 00:00:00 2001 From: Dirk Julich Date: Wed, 1 Jul 2026 13:18:09 +0200 Subject: [PATCH] =?UTF-8?q?AAP-81082=20=E2=80=94=20Eliminate=20LEFT=20OUTE?= =?UTF-8?q?R=20JOINs=20in=20unified=20job=20RBAC=20query=20(#16527)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * AAP-81082 Eliminate LEFT OUTER JOINs in unified job RBAC query UnifiedJobAccess.filtered_queryset() used field-traversal Q objects (inventoryupdate__inventory_source__inventory__id__in=...) which Django translates into LEFT OUTER JOINs. This forces PostgreSQL to scan all rows in main_unifiedjob before filtering — at scale, 99.84% are discarded. Replace with pk__in subqueries that generate IN (SELECT ...) instead, allowing PostgreSQL to skip the unconditional join. EXPLAIN ANALYZE shows a 28% reduction in execution time (496ms -> 355ms), with larger gains expected under concurrent load. * AAP-81082 Eliminate LEFT OUTER JOINs in RBAC filtered_queryset methods Replace field-traversal Q objects with pk__in subqueries across all Access classes that query polymorphic or M2M tables, preventing Django from generating unconditional LEFT OUTER JOINs. Also migrate legacy _accessible_pk_qs / accessible_pk_qs calls to DAB RBAC access_ids_qs. Affected: UnifiedJobAccess, UnifiedJobTemplateAccess, JobAccess, JobEventAccess, LabelAccess. * Fix docstring --- awx/main/access.py | 46 +++++++++++++++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/awx/main/access.py b/awx/main/access.py index 1dc1fc8c39..05c67d06c4 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -1665,11 +1665,11 @@ class JobAccess(BaseAccess): def filtered_queryset(self): qs = self.model.objects - qs_jt = qs.filter(job_template__in=JobTemplate.access_qs(self.user, 'view')) - - org_access_qs = Organization.objects.filter(Q(admin_role__members=self.user) | Q(auditor_role__members=self.user)) + org_access_qs = Organization.objects.filter( + Q(pk__in=Organization.access_ids_qs(self.user, 'change')) | Q(pk__in=Organization.access_ids_qs(self.user, 'audit_organization')) + ) if not org_access_qs.exists(): - return qs_jt + return qs.filter(job_template__in=JobTemplate.access_qs(self.user, 'view')) return qs.filter(Q(job_template__in=JobTemplate.access_qs(self.user, 'view')) | Q(organization__in=org_access_qs)).distinct() @@ -2309,7 +2309,7 @@ class JobHostSummaryAccess(BaseAccess): class JobEventAccess(BaseAccess): """ - I can see job event records whenever I can read both job and host. + I can see job event records whenever I can read the job or the host. """ model = JobEvent @@ -2320,8 +2320,8 @@ class JobEventAccess(BaseAccess): def filtered_queryset(self): return self.model.objects.filter( - Q(host__inventory__in=Inventory.accessible_pk_qs(self.user, 'read_role')) - | Q(job__job_template__in=JobTemplate.accessible_pk_qs(self.user, 'read_role')) + Q(host_id__in=Host.objects.filter(inventory__in=Inventory.access_ids_qs(self.user, 'view')).values('pk')) + | Q(job_id__in=Job.objects.filter(job_template__in=JobTemplate.access_ids_qs(self.user, 'view')).values('pk')) ) def can_add(self, data): @@ -2451,7 +2451,11 @@ class UnifiedJobTemplateAccess(BaseAccess): def filtered_queryset(self): return self.model.objects.filter( Q(pk__in=self.model.accessible_pk_qs(self.user, 'read_role')) - | Q(inventorysource__inventory__id__in=Inventory._accessible_pk_qs(Inventory, self.user, 'read_role')) + | Q( + pk__in=InventorySource.objects.filter( + inventory__id__in=Inventory.access_ids_qs(self.user, 'view'), + ).values('unifiedjobtemplate_ptr_id') + ) ) def can_start(self, obj, validate_license=True): @@ -2497,12 +2501,20 @@ class UnifiedJobAccess(BaseAccess): # ) def filtered_queryset(self): - inv_pk_qs = Inventory._accessible_pk_qs(Inventory, self.user, 'read_role') + inv_pk_qs = Inventory.access_ids_qs(self.user, 'view') qs = self.model.objects.filter( Q(unified_job_template_id__in=UnifiedJobTemplate.accessible_pk_qs(self.user, 'read_role')) - | Q(inventoryupdate__inventory_source__inventory__id__in=inv_pk_qs) - | Q(adhoccommand__inventory__id__in=inv_pk_qs) - | Q(organization__in=Organization.accessible_pk_qs(self.user, 'auditor_role')) + | Q( + pk__in=InventoryUpdate.objects.filter( + inventory_source__inventory__id__in=inv_pk_qs, + ).values('pk') + ) + | Q( + pk__in=AdHocCommand.objects.filter( + inventory__id__in=inv_pk_qs, + ).values('pk') + ) + | Q(organization__in=Organization.access_ids_qs(self.user, 'audit_organization')) ) return qs @@ -2622,9 +2634,13 @@ class LabelAccess(BaseAccess): def filtered_queryset(self): return self.model.objects.filter( - Q(organization__in=Organization.accessible_pk_qs(self.user, 'read_role')) - | Q(unifiedjobtemplate_labels__in=UnifiedJobTemplate.accessible_pk_qs(self.user, 'read_role')) - ).distinct() + Q(organization__in=Organization.access_ids_qs(self.user, 'view')) + | Q( + pk__in=UnifiedJobTemplate.labels.through.objects.filter( + unifiedjobtemplate_id__in=UnifiedJobTemplate.accessible_pk_qs(self.user, 'read_role'), + ).values('label_id') + ) + ) @check_superuser def can_add(self, data):