diff --git a/web/views.py b/web/views.py index b4d485749..869d42011 100644 --- a/web/views.py +++ b/web/views.py @@ -286,14 +286,56 @@ def index(request): :10 ] # Get more and then sort by clicks - # Add click counts manually since WebRequest.user is a CharField, not a ForeignKey - for referrer in top_referrers: - # Look for both new format /ref/CODE/ and old format ?ref=CODE - ref_code = referrer.referral_code - clicks = WebRequest.objects.filter( - models.Q(path__contains=f"/ref/{ref_code}/") | models.Q(path__contains=f"?ref={ref_code}") - ).count() - referrer.total_clicks = clicks + # PERF FIX: Batch-fetch all click counts in a single query instead of N separate + # queries (one per referrer). This avoids an N+1 query problem that runs + # unindexed LIKE scans against a potentially huge WebRequest table on every + # homepage load. + if top_referrers: + all_codes = sorted([r.referral_code for r in top_referrers if r.referral_code], key=len, reverse=True) + + if all_codes: + # Build a single combined Q expression for all codes at once + combined_q = models.Q() + for code in all_codes: + # Broadened contains check to catch /ref/CODE with or without trailing slash + combined_q |= models.Q(path__contains=f"/ref/{code}") | models.Q(path__contains=f"ref={code}") + + # Fetch unique matching paths and their aggregate counts in one go + # Using Sum("count") to support models where rows represent multiple hits + matching_requests = ( + WebRequest.objects.filter(combined_q) + .values("path") + .annotate(total_hits=models.Sum("count")) + ) + + from urllib.parse import parse_qs, urlparse + + # Map each referral code to its click count in-memory + click_counts = {code: 0 for code in all_codes} + for req in matching_requests: + path = req["path"] + total_hits = req["total_hits"] or 0 + extracted_code = None + + # Try new format /en/ref/CODE/ or /en/ref/CODE + if "/ref/" in path: + # Improved regex to handle trailing slashes, end of path, query params, or fragments + m = re.search(r"/ref/([^/?#]+)(?:/|$|\?|#)", path) + if m: + extracted_code = m.group(1) + + # Try query param format ?ref=CODE + if not extracted_code and "ref=" in path: + parsed_url = urlparse(path) + params = parse_qs(parsed_url.query) + if "ref" in params: + extracted_code = params["ref"][0] + + if extracted_code and extracted_code in click_counts: + click_counts[extracted_code] += total_hits + + for referrer in top_referrers: + referrer.total_clicks = click_counts.get(referrer.referral_code, 0) # Re-sort to include click count in ranking top_referrers = sorted(