Skip to content

Commit f6efa56

Browse files
author
Andrew Cheng
committed
Optimize q filter to use pk__in subqueries instead of set operations
The `q` complex filter composed its NOT/AND/OR operands with QuerySet .difference()/.intersection()/.union(), which compile to SQL EXCEPT/INTERSECT/UNION. For NOT in particular, the left operand is the full unfiltered queryset, so evaluating an expression like `repository_version=X AND NOT repository_version=Y` seq-scans the entire content table and sorts/hashes every leg, spilling to disk on large repositories. Rewrite the three operand evaluators to compose with `pk__in` subqueries: - _NotAction: qs.exclude(pk__in=expr.evaluate(qs).values("pk")) - _AndAction: chained qs.filter(pk__in=...) semi-joins - _OrAction: OR of Q(pk__in=...) subqueries This collapses the plan to indexed anti-/semi-joins with no full-table scan, no SetOp, and no disk-spilling sorts. Results are unchanged (each operand still filters the same base queryset on a unique pk, so the implicit de-duplication of the set operations is preserved); the existing q filter functional tests pass without modification. Benchmarked on a 200k-unit repository computing a 1000-unit diff between two repository versions (steady-state, work_mem=4MB): Version sizes Before (set-ops) After (pk__in) --------------- ------------------ ---------------- 199k units ~1400 ms ~680 ms 50k units ~450 ms ~150 ms Planning time also drops (e.g. ~1.4 ms -> ~0.5 ms) as the large inlined operand lists are replaced by parameterized subqueries. Closes #7831 Assisted by: Claude Opus 4.8
1 parent 0979b88 commit f6efa56

2 files changed

Lines changed: 12 additions & 7 deletions

File tree

CHANGES/7831.bugfix

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Optimized the ``q`` complex filter to compose ``NOT``/``AND``/``OR`` operands using ``pk__in``
2+
subqueries instead of the ``EXCEPT``/``INTERSECT``/``UNION`` set operations, avoiding full-table
3+
scans and disk-spilling sorts on large content sets.

pulpcore/filters.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -215,27 +215,29 @@ def __init__(self, tokens):
215215
self.complexity = self.expr.complexity + 1
216216

217217
def evaluate(self, qs):
218-
return qs.difference(self.expr.evaluate(qs))
218+
return qs.exclude(pk__in=self.expr.evaluate(qs).values("pk"))
219219

220220
class _AndAction:
221221
def __init__(self, tokens):
222222
self.exprs = tokens[0]
223223
self.complexity = sum((expr.complexity for expr in self.exprs)) + 1
224224

225225
def evaluate(self, qs):
226-
return (
227-
self.exprs[0]
228-
.evaluate(qs)
229-
.intersection(*[expr.evaluate(qs) for expr in self.exprs[1:]])
230-
)
226+
result = qs
227+
for expr in self.exprs:
228+
result = result.filter(pk__in=expr.evaluate(qs).values("pk"))
229+
return result
231230

232231
class _OrAction:
233232
def __init__(self, tokens):
234233
self.exprs = tokens[0]
235234
self.complexity = sum((expr.complexity for expr in self.exprs)) + 1
236235

237236
def evaluate(self, qs):
238-
return self.exprs[0].evaluate(qs).union(*[expr.evaluate(qs) for expr in self.exprs[1:]])
237+
query = models.Q()
238+
for expr in self.exprs:
239+
query |= models.Q(pk__in=expr.evaluate(qs).values("pk"))
240+
return qs.filter(query)
239241

240242
def __init__(self, *args, **kwargs):
241243
self.filterset = kwargs.pop("filter").parent

0 commit comments

Comments
 (0)