Audyt indeksów i zapytań (czerwiec 2026)¶
Audyt przeprowadzony na zrzucie produkcyjnym db-backup-20260603-023000
(258 tabel, 1048 indeksów, 478 FK, ~830 MB danych skompresowanych).
Analiza schematu: pg_restore --schema-only + parser w Pythonie
(duplikaty / redundantne prefiksy / nieindeksowane FK), rozmiary tabel
z .dat.gz przez pg_restore -l.
Uwaga metodologiczna: statystyki użycia indeksów (
pg_stat_user_indexes.idx_scan) to stan runtime — nie ma ich w zrzucie. Ten audyt wykrywa redundancję strukturalną (duplikaty, prefiksy, brakujące FK), niezależną od ruchu. Analiza „idx_scan=0" była robiona osobno na żywej bazie (PR #315).
Wnioski ogólne¶
- Baza jest prze-zaindeksowana, nie pod-zaindeksowana. Na 478 FK tylko
2 nie mają wspierającego indeksu (oba na tabelach
*_matprzebudowywanych triggerem — patrz niżej). - Źródło redundancji: powtarzalny anty-wzorzec w modelach —
Meta.indexes = [models.Index(fields=["<fk>"])]dla kolumn, które Django już indeksuje (FK ma domyślniedb_index=True), albo które pokrywaunique_together/UniqueConstraint. Efekt: 2 identyczne indeksy lub jednokolumnowy indeks będący redundantnym prefiksem złożonego. - Każdy zbędny indeks = narzut na
INSERT/UPDATE/DELETE(write amplification) + miejsce, przy zerowej korzyści przy odczycie.
Reguły naprawy (wzorzec)¶
| Sytuacja | Naprawa | Migracja Django generuje |
|---|---|---|
Duplikat z models.Index(fields=[fk]) |
usuń linię z Meta.indexes (auto-indeks FK zostaje) |
RemoveIndex |
| Auto-indeks FK = redundantny prefiks złożonego | db_index=False na ForeignKey |
AlterField |
| Indeks osierocony (nie ma w stanie modeli) | raw DROP INDEX CONCURRENTLY IF EXISTS |
SeparateDatabaseAndState |
db_index=False na FK jest bezpieczne także dla CASCADE/PROTECT: złożony
indeks z FK jako kolumną wiodącą obsługuje i lookup, i skan wierszy-dzieci
przy usuwaniu / sprawdzaniu integralności.
AlterField (drop przez introspekcję) jest odporniejszy niż raw
DROP INDEX IF EXISTS <nazwa>: Django znajduje faktyczny indeks na kolumnie
niezależnie od jego nazwy (starych hashy _e76def89 itp.), więc nie
„przegapi" go po cichu na innym wdrożeniu.
Zrobione (zweryfikowane: makemigrations --check czysto, manage.py check OK)¶
ewaluacja_optymalizacja→ migracja0015_…— usuwa 10 indeksów (7 duplikatów zMeta.indexes+ 3 prefiksy FK przezdb_index=False):disciplineswapopportunity(uczelnia/current/target_discipline),optimizationauthorresult(optimization_run+autor / autor / optimization_run prefix),optimizationpublication(author_result / rekord_id),optimizationrun(dyscyplina_naukowa prefix),unpinningopportunity(uczelnia prefix).bppcore → migracja0423_…—db_index=Falsena FKrekordwWydawnictwo_Ciagle_Autor(7.6 MB, hot),Wydawnictwo_Zwarte_Autor(4.2 MB),Patent_Autor— redundantne prefiksyunique_together.
Do zrobienia — checklist (≈60 indeksów, pogrupowane)¶
Ten sam wzorzec naprawy. Pominąć pakiety zewnętrzne (ich indeksy
pochodzą z własnych migracji — nie edytować modeli): taggit_taggeditem,
robots_rule_*, reversion_version, django_template_sites,
celeryui_report.
Modele Django (czyste db_index=False / usunięcie Meta.Index)¶
pbn_komparator_zrodel.RozbieznoscZrodlaPbn(8 — w tym duplikatyMeta.indexesna FK + prefiksyzrodlo_id)komparator_pbn_udzialy.BrakAutorawPublikacji(5),…RozbieznoscDyscyplinPbn(4)deduplikator_publikacji.PublicationDuplicateCandidate(4),deduplikator_autorow.DuplicateCandidate(3),…LogScalania(2),deduplikator_zrodel.NotaDuplicate(1)przemapuj_zrodlo.PrzemapowaZrodla(4)ewaluacja_metryki.MetrykaAutora(3 — prefiksy FK autor/dyscyplina/jednostka)ewaluacja2021.*,ewaluacja_liczba_n.*(po 1 — prefiks FK przedunique_together)pbn_wysylka_oswiadczen.PbnWysylkaLog(2),pbn_import.*(3),powiazania_autorow.AuthorConnection(1),importer_publikacji.ImportedAuthor(1),import_dyscyplin.*(1),flexible_reports.*(2),formdefaults.*(1),dynamic_columns.*(1),favicon.FaviconImg(1),zglos_publikacje.*(1),bpp.Ukryj_Status_Korekty(1)
bpp core — wymagają ostrożności (abstrakty / indeksy osierocone)¶
bpp_wydawnictwo_zwarte.wydawca(20 MB) — FKwydawcaw abstrakcieWydawnictwo_Zwarte_Baza; prefiks(wydawca, rok). Sprawdzić wszystkie konkretne podklasy zanim ustawi siędb_index=Falsew bazie (czy każda ma złożony(wydawca, rok)).bpp_praca_doktorska/bpp_praca_habilitacyjna— FKwydawca(prefiks) wPraca_Doktorska_Bazaoraz osierocone…_52be3978naautor_id(duplikat OneToOne/FK; nie ma w stanie modeli → rawDROP INDEX CONCURRENTLY).
Tabele *_mat (raw SQL, managed=False) — osobny mechanizm¶
Indeksy zarządzane surowym SQL-em w migracjach (nie przez Meta):
- bpp_autorzy_mat: _2 (autor_id) ⊂ _4 (autor, jednostka);
_6 (dyscyplina) ⊂ _8 (dyscyplina, rekord) — prefiksy do usunięcia
raw-em.
- Brakujące FK (jedyne 2 w całej bazie):
bpp_autorzy_mat.typ_odpowiedzialnosci_id →
bpp_typ_odpowiedzialnosci, bpp_rekord_mat.konferencja_id →
bpp_konferencja. Ocenić, czy potrzebne (tabele przebudowywane
triggerem; FK bez indeksu spowalnia kaskady/joiny, ale przy pełnej
przebudowie może nie mieć znaczenia).
Audyt zapytań — multiseek / front / admin¶
Hot-paths są dobrze zoptymalizowane — brak oczywistych N+1:
- multiseek (
bpp/views/mymultiseek.py):.only("id", "opis_bibliograficzny_cache")(pomija ciężkiVectorFieldw widoku listy),select_related("charakter_formalny", "typ_kbn")dla raportów tabelarycznych, sumy przezaggregate(Sum(...))po stronie DB. Opis bibliograficzny jest precomputowany (opis_bibliograficzny_cachenabpp_rekord_mat). - Code smell (drobny): linia ~74 —
sql = str(ret.query)i podstring"bpp_autorzy_mat" in sqlżeby zdecydować o.distinct(); kompilacja całego SQL-a na każdy request. Komentarz sam przyznaje „not ideal". - front (
bpp/views/browse.py): listy (AutorzyView,ZrodlaView,JednostkiView) używają.only()+select_related. Strona autora doładowuje listę publikacji AJAX-em (brak dużej pętli server-side); pętle w szablonie (metryki,praca_doktorska_set,jednostki…,prace_w_latach) są ograniczone per-autor.
Admin — znalezione N+1¶
- NAPRAWIONE:
Autor_DyscyplinaAdmin(bpp/admin/autor_dyscyplina.py) —list_displaymiał 4 kolumny FK (autor,rodzaj_autora,dyscyplina_naukowa,subdyscyplina_naukowa) + metodyorcid/pbn_uid_idczytająceobj.autor.*, bezlist_select_related→ ~5 zapytań × 100 wierszy/stronę. Dodanolist_select_related. - Do rozważenia (admin-only, niższy priorytet):
- Admin-y
pbn_api(scientist337 MB,journal128 MB,publisher,publication): metodyrekord_w_bpp/from_institution_apiwlist_displayrobią per-wiersz lookup cross-model. Nie naprawi tego prostelist_select_related(to reverse-lookup; trzeba prefetch / annotate / cache). Admin = niski ruch, ale tabele ogromne. oplaty_logadmin:get_publikacja(GenericFK) +changed_by(User FK) bezlist_select_related.
Znaleziska poboczne¶
- Zepsuty pakiet w venv:
djangoql-iplweb==0.21.0(release 2026-06-03) zainstalował się niekompletnie (tylkoextras.py+locale/, bez__init__.py/admin.py) →manage.pyw ogóle nie wstawał.uv pip install --reinstall-package djangoql-iplwebnaprawił. Sprawdzić, czy wheel na PyPI nie jest wadliwy (jeśli CI/prod zainstaluje tak samo — wybuchnie). - Retencja logów (decyzje usera): logi edycji (
easyaudit_crudevent108 MB,reversion_version, adminLogEntry) — zostają na zawsze. Logi logowania (easyaudit_loginevent) — 24 miesiące (do zaimplementowania: zadanie Celery beat). Obecnie brak jakiejkolwiek retencji. - Brak ochrony przed brute-force na klasycznym logowaniu (brak
django-axes/defender/ratelimit; DRF throttling zakomentowany od 2020).
Rekomendacja:
django-axes,FAILURE_LIMIT=5, lockout po(login, IP),COOLOFF=1h. (Wdrażane osobno.)