From 5d32091e1621f1451c0f3a4adb73765450f956e7 Mon Sep 17 00:00:00 2001 From: Matti Eiden Date: Fri, 23 Jan 2026 12:22:42 +0200 Subject: [PATCH 1/5] feat: support enabling django debug toolbar with DEBUG_TOOLBAR env var If DEBUG and DEBUG_TOOLBAR are set, django-debug-toolbar will be enabled and configured. Refs: RATYK-156 --- geo_search/settings.py | 8 ++++++++ geo_search/urls.py | 6 ++++++ 2 files changed, 14 insertions(+) diff --git a/geo_search/settings.py b/geo_search/settings.py index e6fa5bc..c17f4a8 100644 --- a/geo_search/settings.py +++ b/geo_search/settings.py @@ -23,6 +23,7 @@ env = Env( DEBUG=(bool, False), + DEBUG_TOOLBAR=(bool, False), SECRET_KEY=(str, "temp_key"), ALLOWED_HOSTS=(list, []), STATIC_ROOT=(str, str(BASE_DIR / "static")), @@ -47,6 +48,8 @@ Env.read_env(env_path) DEBUG = env.bool("DEBUG") +DEBUG_TOOLBAR = env.bool("DEBUG_TOOLBAR") + SECRET_KEY = env.str("SECRET_KEY") if DEBUG and not SECRET_KEY: SECRET_KEY = "secret-for-debugging-only" @@ -112,6 +115,11 @@ def sentry_traces_sampler(sampling_context: SamplingContext) -> float: "whitenoise.middleware.WhiteNoiseMiddleware", ] +if DEBUG and DEBUG_TOOLBAR: + INSTALLED_APPS.append("debug_toolbar") + MIDDLEWARE.append("debug_toolbar.middleware.DebugToolbarMiddleware") + INTERNAL_IPS = ["127.0.0.1"] + TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", diff --git a/geo_search/urls.py b/geo_search/urls.py index 20f281e..eba004e 100644 --- a/geo_search/urls.py +++ b/geo_search/urls.py @@ -1,3 +1,4 @@ +from django.conf import settings from django.contrib import admin from django.http import HttpResponse from django.urls import include, path @@ -45,3 +46,8 @@ def readiness(*args, **kwargs) -> HttpResponse: urlpatterns += [path("healthz", healthz), path("readiness", readiness)] + +if settings.DEBUG and settings.DEBUG_TOOLBAR: + from debug_toolbar.toolbar import debug_toolbar_urls + + urlpatterns += debug_toolbar_urls() From 331ef77329915b9ecbc5d325d7931642ff1d561d Mon Sep 17 00:00:00 2001 From: Matti Eiden Date: Fri, 23 Jan 2026 13:05:20 +0200 Subject: [PATCH 2/5] perf: replace use of slow DISTINCT On large tables DISTINCT tends to result in a heavy operation with Django. The intention is to remove duplicate rows that can result from filtering via many-to-many relations. This can usually be worked around and in this case we can manually prefetch id numbers to avoid expensive JOINS against the address table. Refs: RATYK-156 --- address/api/views.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/address/api/views.py b/address/api/views.py index d1bcff9..ea46dd0 100644 --- a/address/api/views.py +++ b/address/api/views.py @@ -8,6 +8,8 @@ from rest_framework.exceptions import ParseError from rest_framework.viewsets import ReadOnlyModelViewSet +from address.models import Street + from ..models import Address, Municipality, PostalCodeArea from .serializers import ( AddressSerializer, @@ -169,9 +171,12 @@ def _filter_by_street_name(self, addresses: QuerySet) -> QuerySet: street_name = self.request.query_params.get("streetname") if street_name is None: return addresses - return addresses.filter( - street__translations__name__iexact=street_name - ).distinct() + street_ids = list( + Street.objects.filter(translations__name__iexact=street_name).values_list( + "id", flat=True + ) + ) + return addresses.filter(street_id__in=street_ids) def _filter_by_street_number(self, addresses: QuerySet) -> QuerySet: street_number = self.request.query_params.get("streetnumber") @@ -191,15 +196,23 @@ def _filter_by_municipality(self, addresses: QuerySet) -> QuerySet: municipality = self.request.query_params.get("municipality") if municipality is None: return addresses - return addresses.filter( - municipality__translations__name__iexact=municipality - ).distinct() + municipality_ids = list( + Municipality.objects.filter( + translations__name__iexact=municipality + ).values_list("id", flat=True) + ) + return addresses.filter(municipality_id__in=municipality_ids) def _filter_by_municipality_code(self, addresses: QuerySet) -> QuerySet: municipality_code = self.request.query_params.get("municipalitycode") if municipality_code is None: return addresses - return addresses.filter(municipality__code__iexact=municipality_code).distinct() + municipality_ids = list( + Municipality.objects.filter(code=municipality_code).values_list( + "id", flat=True + ) + ) + return addresses.filter(municipality_id__in=municipality_ids) def _filter_by_postal_code(self, addresses: QuerySet) -> QuerySet: postal_code = self.request.query_params.get("postalcode") From dc6ab80be0b73d05e635c0048a5aa4441a849c1d Mon Sep 17 00:00:00 2001 From: Matti Eiden Date: Fri, 23 Jan 2026 13:08:17 +0200 Subject: [PATCH 3/5] perf: remove pk__gte=0 filters These were originally added to work around bugs in earlier Postgres versions. Presumably should not happen anymore. They seem to have a small performance cost. Refs: RATYK-156 --- address/api/views.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/address/api/views.py b/address/api/views.py index ea46dd0..f12e92d 100644 --- a/address/api/views.py +++ b/address/api/views.py @@ -151,7 +151,7 @@ ) ) class AddressViewSet(ReadOnlyModelViewSet): - queryset = Address.objects.filter(pk__gte=0).order_by("pk") + queryset = Address.objects.order_by("pk") serializer_class = AddressSerializer def get_queryset(self) -> QuerySet: @@ -277,7 +277,7 @@ def _filter_by_location(self, addresses: QuerySet) -> QuerySet: retrieve=extend_schema(parameters=_area_parameters), ) class PostalCodeAreaViewSet(ReadOnlyModelViewSet): - queryset = PostalCodeArea.objects.filter(pk__gte=0).order_by("pk") + queryset = PostalCodeArea.objects.order_by("pk") serializer_class = PostalCodeAreaSerializer def get_queryset(self) -> QuerySet: @@ -320,5 +320,5 @@ def _filter_by_post_office(self, areas: QuerySet) -> QuerySet: retrieve=extend_schema(parameters=_area_parameters), ) class MunicipalityViewSet(ReadOnlyModelViewSet): - queryset = Municipality.objects.filter(pk__gte=0).order_by("pk") + queryset = Municipality.objects.order_by("pk") serializer_class = MunicipalitySerializer From 88693541f9541ec17d0b417d21bc041463b778e0 Mon Sep 17 00:00:00 2001 From: Matti Eiden Date: Fri, 23 Jan 2026 14:14:57 +0200 Subject: [PATCH 4/5] perf: add select_related and prefetch_related Removes a lot of duplicate queries from endpoints by setting appropriate select_related and prefetch_related. Refs: RATYK-156 --- address/api/views.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/address/api/views.py b/address/api/views.py index f12e92d..58d5c88 100644 --- a/address/api/views.py +++ b/address/api/views.py @@ -151,7 +151,16 @@ ) ) class AddressViewSet(ReadOnlyModelViewSet): - queryset = Address.objects.order_by("pk") + queryset = ( + Address.objects.order_by("pk") + # .select_related("street", "postal_code_area", "municipality") + .prefetch_related( + "street__translations", + "postal_code_area__translations", + "municipality__translations", + ) + ) + serializer_class = AddressSerializer def get_queryset(self) -> QuerySet: @@ -277,7 +286,7 @@ def _filter_by_location(self, addresses: QuerySet) -> QuerySet: retrieve=extend_schema(parameters=_area_parameters), ) class PostalCodeAreaViewSet(ReadOnlyModelViewSet): - queryset = PostalCodeArea.objects.order_by("pk") + queryset = PostalCodeArea.objects.order_by("pk").prefetch_related("translations") serializer_class = PostalCodeAreaSerializer def get_queryset(self) -> QuerySet: @@ -320,5 +329,5 @@ def _filter_by_post_office(self, areas: QuerySet) -> QuerySet: retrieve=extend_schema(parameters=_area_parameters), ) class MunicipalityViewSet(ReadOnlyModelViewSet): - queryset = Municipality.objects.order_by("pk") + queryset = Municipality.objects.order_by("pk").prefetch_related("translations") serializer_class = MunicipalitySerializer From 1aaf25bb24df91f2c83869e5e016a4fa8e748bc5 Mon Sep 17 00:00:00 2001 From: Matti Eiden Date: Fri, 23 Jan 2026 17:40:05 +0200 Subject: [PATCH 5/5] perf: add index for address-municipality The index includes all the fields that the address rest endpoint uses which result in very fast index-only scans. refs: RATYK-156 --- address/api/views.py | 12 +++----- ...008_address_idx_address_municipality_id.py | 28 +++++++++++++++++++ address/models.py | 17 +++++++++++ 3 files changed, 49 insertions(+), 8 deletions(-) create mode 100644 address/migrations/0008_address_idx_address_municipality_id.py diff --git a/address/api/views.py b/address/api/views.py index 58d5c88..8ade9f0 100644 --- a/address/api/views.py +++ b/address/api/views.py @@ -151,14 +151,10 @@ ) ) class AddressViewSet(ReadOnlyModelViewSet): - queryset = ( - Address.objects.order_by("pk") - # .select_related("street", "postal_code_area", "municipality") - .prefetch_related( - "street__translations", - "postal_code_area__translations", - "municipality__translations", - ) + queryset = Address.objects.order_by("pk").prefetch_related( + "street__translations", + "postal_code_area__translations", + "municipality__translations", ) serializer_class = AddressSerializer diff --git a/address/migrations/0008_address_idx_address_municipality_id.py b/address/migrations/0008_address_idx_address_municipality_id.py new file mode 100644 index 0000000..3c86f6e --- /dev/null +++ b/address/migrations/0008_address_idx_address_municipality_id.py @@ -0,0 +1,28 @@ +# Generated by Django 6.0 on 2026-01-23 15:38 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("address", "0007_postalcodearea_municipality"), + ] + + operations = [ + migrations.AddIndex( + model_name="address", + index=models.Index( + fields=["municipality", "id"], + include=( + "street", + "number", + "number_end", + "letter", + "postal_code_area", + "location", + "modified_at", + ), + name="idx_address_municipality_id", + ), + ), + ] diff --git a/address/models.py b/address/models.py index 3dcc296..b95c876 100644 --- a/address/models.py +++ b/address/models.py @@ -79,3 +79,20 @@ def __str__(self) -> str: class Meta: verbose_name_plural = _("Addresses") + indexes = [ + # This index speeds up address?municipality and address?municipalitycode + # by 10x + models.Index( + name="idx_address_municipality_id", + fields=["municipality", "id"], + include=[ + "street", + "number", + "number_end", + "letter", + "postal_code_area", + "location", + "modified_at", + ], + ) + ]