perf(admin): cztery N+1 na changelistach — mierzone na kopii produkcji, bez Django 6.1 - #738
Merged
Conversation
Znalezione pomiarem na kopii bazy produkcyjnej (68 tys. autorow),
nie lektura kodu. Oba sprowadzaja sie do relacji dotykanej per wiersz,
ktorej nikt nie zadeklarowal w select_related.
1. AutorAdmin.list_select_related dla kolumny "aktualna_jednostka"
ciagnal sama jednostke i jej wydzial, ale NIE uczelnie. Jednostka.__str__
czyta self.uczelnia.uzywaj_wydzialow, zeby zdecydowac, czy dokleic nazwe
wydzialu -- czyli jeden SELECT na kazdy wiersz. Zmierzone: 50 zapytan
po bpp_uczelnia na stronie 50 autorow.
2. WydzialAutoraFilter.lookups buduje liste rozwijana przez str(j) w petli,
bez select_related("uczelnia") -- jedno zapytanie na kazda pozycje.
Changelista autorow: 102 -> 45 zapytan (mierzone bez regul CACHEOPS).
Dlaczego licznik zapytan tego NIE pokazywal: produkcyjne reguly CACHEOPS
cache'uja bpp.uczelnia, wiec te pobrania nie ida do PostgreSQL, tylko
zamieniaja sie w round-tripy do Redisa. W wariancie bench_prod changelista
autorow ma 27 zapytan przed i po -- caly koszt widac dopiero na zegarze.
Dlatego testy mierza wariant bez cacheops, gdzie N+1 jest widoczne wprost.
Kolumna "aktualna_jednostka" jest opcjonalna (list_display_allowed,
wlaczana per uzytkownik przez dynamic_admin_columns), a
get_list_select_related dokłada JOIN-y tylko dla kolumn aktualnie
widocznych -- wiec poprawka nic nie kosztuje, gdy kolumna jest schowana.
Testy przybijaja niezmiennik "liczba zapytan nie rosnie z liczba wierszy",
a nie konkretna liczbe zapytan -- taka asercja pekalaby przy kazdej
niezwiazanej zmianie. Zweryfikowane, ze bez poprawek oba padaja.
Zmiany sa czystym ORM-em i nie wymagaja Django 6.1.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PjoMtF6KAaJydhGEGibgV7
Przeniesione z PR #736 (gałąź feat/fetch-raise-gate), ktory celowal w django-6.1. Same poprawki to czysty select_related i NIE wymagaja Django 6.1 -- trzymanie ich na gałęzi 6.1 opoznialoby je do czasu scalenia calego upgrade'u. Bramka FETCH_RAISE z tamtego PR-a zostaje na django-6.1, bo ona faktycznie wymaga fetch modes. 1. JednostkaAdmin: ChangeList.get_queryset aplikuje list_select_related TYLKO gdy queryset bazowy nie ma jeszcze zadnego select_related. JednostkaManager.get_queryset() dokłada select_related("wydzial"), wiec warunek byl falszywy i CALA deklaracja przepadala -- rodzaj i uczelnia dociagaly sie per wiersz. Wymuszamy je wprost. 2. Wydawnictwo_ZwarteAdmin: kolumna "wydawnictwo" (domyslnie widoczna) to property czytajace self.wydawca.nazwa, ale list_select_related mapowalo wydawce wylacznie na jawna kolumne "wydawca" -- czyli JOIN praktycznie nigdy nie wchodzil. Zmierzone na kopii bazy produkcyjnej (Django 5.2, produkcyjne CACHEOPS): changelist jednostek 66 -> 16 zapytan changelist wyd. zwartych 70 -> 35 zapytan changelist zrodel (kontroler, nietkniety) 16 -> 16 Dla porownania: te same changelisty na django-6.1 z FETCH_PEERS daja 17 i 36 zapytan, czyli o jedno wiecej. FETCH_PEERS musi dorzucic jedno zapytanie hurtowe na relacje, a select_related zalatwia to JOIN-em. Dolozone bramki regresyjne dzialajace na 5.2 (test_admin_select_related): przybijaja niezmiennik "liczba zapytan nie rosnie z liczba wierszy", a nie konkretna liczbe. Zweryfikowane, ze bez poprawek padaja (RodzajJednostki 2->7, Wydawca 1->6). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PjoMtF6KAaJydhGEGibgV7
mpasternak
added a commit
that referenced
this pull request
Aug 7, 2026
Pomiar wykonany na cichym hoscie, na kopii bazy produkcyjnej. Kryterium z handoffu spelnione: kontroler "admin: zrodlo (changelist)" ma 16 zapytan we wszystkich czterech przebiegach, rozrzut 3,2 ms = 1,1 % mediany, zero flag SZUM. Na mac-mini ten sam kontroler skakal 320 -> 420 ms, przez co tamten przebieg byl do wyrzucenia. Docstring BaseBppAdminMixin.get_queryset podawal 40 i 38 zapytan oraz czasy opisane jako rzad wielkosci -- podmienione na zmierzone (36 i 34). Dopisane dwie rzeczy, ktore pomiar ujawnil, a ktore zmieniaja uzasadnienie FETCH_PEERS: * changelist autorow ma 27 zapytan przed i po, a czas spada o 68 % -- bo dotykane relacje sa cache'owane przez CACHEOPS i koszt to round-tripy do Redisa, niewidoczne dla licznika SQL. Sam ten spadek pochodzi zreszta glownie z poprawek filtrow na dev (5ffc83f), nie z FETCH_PEERS. * PR #738 dokłada na dev jawne select_related dla tych samych relacji i schodzi do 16 i 35 zapytan -- o JEDNO mniej niz FETCH_PEERS, bo ten musi dorzucic zapytanie hurtowe na relacje, a JOIN nie. Po scaleniu te pozycje przestana byc argumentem za FETCH_PEERS; zostaje wlasciwy: siatka bezpieczenstwa na relacje, ktorych nikt nie zadeklarowal. Handoff przestawiony na ZAMKNIETY, z tabela czasow, rozbiciem 5.2 vs 6.1 i nowa pulapka: bench.py ustawia porty przez os.environ.setdefault(), co NIE nadpisuje zmiennej juz wyeksportowanej przez profil shella -- benchmark po cichu uderza wtedy w dev-owe kontenery zamiast w odtworzony dump. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PjoMtF6KAaJydhGEGibgV7
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Cztery N+1 w adminie, znalezione pomiarem na kopii bazy produkcyjnej
(68 tys. autorów, 491 jednostek), nie lekturą kodu. Wszystkie to czysty
select_related— działają na Django 5.2 i nie czekają na upgrade do 6.1.Wynik
Mierzone przez
bench/bench_orm.py --pomiarw warianciebench_prod(produkcyjne reguły
CACHEOPS), Django 5.2.16:FETCH_PEERS* changelista autorów mierzona bez reguł cacheops — z nimi ma 27 zapytań
przed i po, bo
bpp.uczelniajest cache'owana i pobrania idą do Redisazamiast do PostgreSQL. Licznik SQL ich nie widzi, zegar widzi.
Warto odnotować: na 5.2 wychodzimy o jedno zapytanie lepiej niż 6.1
z
FETCH_PEERS.FETCH_PEERSmusi dorzucić jedno zapytanie hurtowe narelację, a
select_relatedzałatwia to JOIN-em.Co konkretnie
AutorAdmin—list_select_relateddla kolumnyaktualna_jednostkaciągnął jednostkę i jej wydział, ale nie uczelnię, a
Jednostka.__str__czyta
self.uczelnia.uzywaj_wydzialow. 50 zapytań na stronę 50 autorów.Kolumna jest opcjonalna (
list_display_allowed), aget_list_select_relateddokłada JOIN-y tylko dla kolumn aktualnie widocznych — więc poprawka nic
nie kosztuje, gdy kolumna jest schowana.
WydzialAutoraFilter.lookups— buduje listę rozwijaną przezstr(j)w pętli, bez
select_related("uczelnia"). Jedno zapytanie na pozycję.JednostkaAdmin(z Bramka regresyjna na N+1 w adminie (FETCH_RAISE) + dwa realne N+1 #736) —ChangeList.get_querysetaplikujelist_select_relatedtylko gdy queryset bazowy nie ma jeszcze żadnegoselect_related, aJednostkaManagerdokładaselect_related("wydzial")— więc cała deklaracja przepadała.
Wydawnictwo_ZwarteAdmin(z Bramka regresyjna na N+1 w adminie (FETCH_RAISE) + dwa realne N+1 #736) — kolumnawydawnictwo(domyślnie widoczna) czyta
self.wydawca.nazwaprzez property, a wydawcabył zmapowany wyłącznie na jawną kolumnę
wydawca.Relacja do #736 (zmergowany do
django-6.1)Pozycje 3 i 4 pochodzą z #736, który celował w
django-6.1i został tamw międzyczasie zmergowany (
5635e0394). Same poprawki nie wymagają 6.1,więc trafiają tu, żeby użytkownicy 5.2 nie czekali na scalenie całego
upgrade'u. Bramka
FETCH_RAISEz #736 zostaje nadjango-6.1— onafaktycznie wymaga fetch modes; tu zastępują ją bramki działające na 5.2.
Skutek uboczny: te dwa pliki są chwilowo w obu gałęziach. Treść jest
identyczna poza jednym docstringiem w
jednostka.py(przekierowany na testdziałający na 5.2), więc przy scalaniu
django-6.1→devbędzie conajwyżej drobny konflikt w komentarzu.
Testy
src/bpp/tests/test_admin/test_admin_select_related.py— cztery bramkiprzybijające niezmiennik „liczba zapytań nie rośnie z liczbą wierszy",
a nie konkretną liczbę zapytań (taka asercja pękałaby przy każdej
niezwiązanej zmianie). Każda zweryfikowana pod kątem tego, że bez
poprawki faktycznie płonie: Uczelnia 2→6, RodzajJednostki 2→7,
Wydawca 1→6.
src/bpp/tests/test_admin/+src/bpp/tests/test_views/: 1016 passed,1 skipped (łącznie z testami Playwright).
🤖 Generated with Claude Code
https://claude.ai/code/session_01PjoMtF6KAaJydhGEGibgV7