Django i PostgreSQL w 2026: indeksy, wyszukiwanie pełnotekstowe i pytania rekrutacyjne
Praktyczny przewodnik po optymalizacji Django z PostgreSQL: indeksy B-tree, częściowe i pokrywające, wyszukiwanie pełnotekstowe z SearchVector i GIN oraz pytania rekrutacyjne 2026.

Optymalizacja Django z PostgreSQL decyduje o tym, czy aplikacja skaluje się poza pierwszy tysiąc użytkowników, czy dławi się przy każdym widoku listy. PostgreSQL dostarcza silnik indeksowania i podsystem wyszukiwania pełnotekstowego, których większość projektów Django w ogóle nie rusza, marnując wydajność zapytań, której odzyskanie nic nie kosztuje. Ten przewodnik omawia wzorce indeksowania, narzędzia wyszukiwania z django.contrib.postgres oraz pytania rekrutacyjne o bazy danych, które w 2026 roku odróżniają inżyniera Django na poziomie mid od seniora.
Każdy indeks przyspiesza odczyty, ale spowalnia zapisy i zajmuje miejsce na dysku. Warto uruchomić EXPLAIN (ANALYZE, BUFFERS) na rzeczywistym zapytaniu, dodać indeks, a następnie porównać plan. Indeks, którego planer zapytań nigdy nie wybiera, to czysty narzut przy każdym poleceniu INSERT i UPDATE.
Podstawy indeksowania PostgreSQL w Django
Domyślnie PostgreSQL tworzy indeks B-tree tylko na kluczach głównych i kolumnach z unique=True. Każda inna filtrowana lub sortowana kolumna korzysta ze skanu sekwencyjnego, dopóki nie powstanie dla niej indeks. Django udostępnia indeksy przez Meta.indexes, co jest lepszym rozwiązaniem niż starsze db_index=True, ponieważ w jednym miejscu obsługuje indeksy złożone, częściowe i pokrywające.
Kolejność kolumn w indeksie złożonym nie jest kwestią kosmetyczną. B-tree może wykorzystać indeks do zapytania tylko wtedy, gdy filtr używa wiodącego prefiksu indeksowanych kolumn, co szczegółowo wyjaśnia Use The Index, Luke. Indeks na (status, created_at) obsłuży WHERE status = ? ORDER BY created_at, ale ten sam indeks nie przyspieszy zapytania filtrującego wyłącznie po created_at.
# models.py
from django.db import models
class Order(models.Model):
customer_email = models.EmailField()
status = models.CharField(max_length=20)
total = models.DecimalField(max_digits=10, decimal_places=2)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
indexes = [
# Jednokolumnowy B-tree do wyszukiwań dokładnych i zakresowych po status
models.Index(fields=["status"]),
# Indeks złożony: obsługuje WHERE status = ? ORDER BY created_at DESC
# Kolumna wiodąca (status) musi wystąpić w filtrze, aby indeks zadziałał
models.Index(fields=["status", "-created_at"]),
]Indeks złożony zamienia zapytanie pulpitu w rodzaju „najnowsze oczekujące zamówienia” w pojedynczy skan indeksu, zamiast filtrowania i sortowania całej tabeli.
Indeksy częściowe i pokrywające do zapytań celowanych
Indeks częściowy obejmuje tylko wiersze spełniające warunek, dzięki czemu pozostaje mały i mieści się w pamięci. Gdy większość wierszy ma tę samą wartość, a zapytania sięgają wyłącznie po mniejszość, indeks częściowy jest radykalnie tańszy od pełnego. Indeks pokrywający idzie dalej: przez dodanie kolumn niekluczowych za pomocą include PostgreSQL odpowiada na zapytanie bezpośrednio z indeksu, bez sięgania do sterty tabeli, w schemacie dostępu zwanym index-only scan.
# models.py
from django.db import models
from django.db.models import Q
class Order(models.Model):
# pola zdefiniowane powyżej
class Meta:
indexes = [
# Indeks częściowy: indeksuje tylko wiersze pending, pomijając zakończoną historię
models.Index(
fields=["created_at"],
name="pending_orders_idx",
condition=Q(status="pending"),
),
# Indeks pokrywający: total czytany prosto z indeksu (index-only scan)
models.Index(
fields=["status"],
include=["total"],
name="status_total_covering_idx",
),
]W tabeli zamówień, w której 2% wierszy ma status pending, indeks częściowy może być pięćdziesiąt razy mniejszy od pełnego indeksu na tej samej kolumnie, a planer utrzymuje go rozgrzany w pamięci podręcznej buforów. Klauzula include jest opisana w dokumentacji PostgreSQL index-only scans i stanowi najczęściej pomijaną korzyść w aplikacjach Django, które agregują kolumnę liczbową za filtrem po statusie.
Wyszukiwanie pełnotekstowe w Django z PostgreSQL
Sięgnięcie po title__icontains=query wydaje się wygodne, ale wymusza skan sekwencyjny i nie potrafi rankingować wyników według trafności. Wyszukiwanie pełnotekstowe w Django opakowuje natywne typy tsvector i tsquery z PostgreSQL przez SearchVector, SearchQuery i SearchRank. Wektor wyszukiwania normalizuje tekst do leksemów, usuwając słowa stopu i sprowadzając wyrazy do ich rdzeni, dzięki czemu wyszukanie „running” trafia w „run” oraz „ran”.
# views.py
from django.contrib.postgres.search import (
SearchVector, SearchQuery, SearchRank,
)
from .models import Article
def search_articles(query_text):
query = SearchQuery(query_text, config="english")
# Waga A stawia trafienia w tytule ponad trafieniami w treści (waga B)
vector = (
SearchVector("title", weight="A")
+ SearchVector("body", weight="B")
)
return (
Article.objects.annotate(rank=SearchRank(vector, query))
.filter(rank__gte=0.1)
.order_by("-rank")
)Parametr weight przypisuje koszyki priorytetu od A (najwyższy) do D. Trafienie w tytule jest oceniane wyżej niż trafienie w treści, nawet gdy oba zawierają szukane wyrażenie, co odpowiada temu, jak użytkownicy oczekują działania wyszukiwarki. Pełne API oraz czterostopniowy model wag opisuje dokumentacja wyszukiwania pełnotekstowego Django.
Indeksy GIN i SearchVectorField dla szybkiego wyszukiwania
Powyższe zapytanie przelicza wektor wyszukiwania dla każdego wiersza przy każdym żądaniu, co jest w porządku dla kilku tysięcy wierszy, ale nie do przyjęcia dla miliona. Produkcyjny wzorzec przechowuje wektor w polu SearchVectorField i indeksuje go indeksem GIN, czyli typem indeksu, którego PostgreSQL używa dla wartości złożonych, takich jak dokumenty pełnotekstowe i JSONB.
# models.py
from django.contrib.postgres.search import SearchVectorField
from django.contrib.postgres.indexes import GinIndex
from django.db import models
class Article(models.Model):
title = models.CharField(max_length=255)
body = models.TextField()
# Przechowywany, wstępnie policzony dokument wyszukiwania
search_vector = SearchVectorField(null=True)
class Meta:
indexes = [
# GIN zamienia wyszukiwania pełnotekstowe w logarytmiczne skany indeksu
GinIndex(fields=["search_vector"]),
]Przechowywane pole musi pozostawać zsynchronizowane z title i body. Sygnał post_save, który zapisuje wektor za pomocą .update(), unika rekurencyjnego wyzwolenia samego siebie, ponieważ QuerySet.update() nie emituje sygnałów zapisu.
# signals.py
from django.contrib.postgres.search import SearchVector
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import Article
@receiver(post_save, sender=Article)
def sync_search_vector(sender, instance, **kwargs):
Article.objects.filter(pk=instance.pk).update(
search_vector=SearchVector("title", weight="A")
+ SearchVector("body", weight="B"),
)Dla zespołów pracujących na Django 5.0 lub nowszym, obliczane po stronie bazy GeneratedField lub wyzwalacz PostgreSQL całkowicie eliminują dodatkowe UPDATE, utrzymując kolumnę wewnątrz bazy danych. Niezależnie od mechanizmu, który ją wypełnia, zapytanie filtruje wówczas indeksowane pole bezpośrednio, a indeks GIN zamienia dawny pełny skan tabeli w logarytmiczne wyszukiwanie.
Indeksy GIN przyspieszają odczyty kosztem zapisów, ponieważ każde wstawienie dotyka wielu wpisów indeksu. W tabelach o intensywnym zapisie warto ustawić parametr przechowywania fastupdate i monitorować rozmiar listy oczekujących, w przeciwnym razie zapisy pełnotekstowe będą się zatrzymywać za utrzymaniem indeksu.
Wybór właściwego typu indeksu PostgreSQL
B-tree jest właściwym ustawieniem domyślnym, ale PostgreSQL udostępnia kilka typów indeksów przez django.contrib.postgres.indexes, a wybór niewłaściwego marnuje miejsce na dysku bez przyspieszenia choćby jednego zapytania. Wybór wynika z kształtu danych oraz operatora używanego przez zapytanie, co zestawia dokumentacja typów indeksów PostgreSQL.
| Typ indeksu | Najlepszy do | Klasa Django |
|------------|----------|--------------|
| B-tree | Równość i zakres na kolumnach skalarnych | models.Index |
| GIN | Wyszukiwanie pełnotekstowe, JSONB, zawieranie w tablicach | GinIndex |
| BRIN | Ogromne tabele tylko do dopisywania, uporządkowane po kolumnie | BrinIndex |
| Hash | Wyszukiwania wyłącznie po równości na dużych kolumnach | HashIndex |
BRIN zasługuje na szczególną uwagę przy danych szeregów czasowych. W tabeli z dziesiątkami milionów wierszy wstawianych w kolejności znaczników czasu BrinIndex na created_at zajmuje kilka kilobajtów tam, gdzie B-tree potrzebowałby setek megabajtów, ponieważ BRIN przechowuje jedynie wartość minimalną i maksymalną dla zakresu bloków. Kompromis polega na tym, że BRIN pomaga tylko wtedy, gdy fizyczna kolejność wierszy podąża za indeksowaną kolumną, co naturalnie zachodzi dla logów i tabel zdarzeń tylko do dopisywania.
# models.py
from django.contrib.postgres.indexes import BrinIndex
from django.db import models
class Event(models.Model):
payload = models.JSONField()
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
indexes = [
# Maleńki indeks do skanów zakresowych na tabeli tylko do dopisywania, uporządkowanej czasowo
BrinIndex(fields=["created_at"]),
]Dopasowanie typu indeksu do wzorca dostępu to powracający temat rozmów na stanowiska senior, ponieważ dowodzi, że kandydat rozumuje o układzie przechowywania danych, zamiast odruchowo dodawać B-tree wszędzie.
Gotowy na rozmowy o Django?
Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.
Pytania rekrutacyjne o bazy danych w Django w 2026
Pytania o bazy danych dominują na rozmowach na stanowiska senior w Django, ponieważ biegłość w ORM nie oznacza rozumienia tego, co ORM faktycznie generuje. Poniższe pytania odzwierciedlają to, co komisje rekrutacyjne rzeczywiście sprawdzają w 2026 roku, i naturalnie uzupełniają dostrajanie na poziomie zapytań opisane w przewodniku optymalizacji zapytań Django ORM.
Kiedy indeks złożony na (a, b) nie pomaga zapytaniu? Gdy zapytanie nie filtruje po a. B-tree jest uporządkowany najpierw według swojej kolumny wiodącej, więc filtr wyłącznie po b nie może użyć indeksu. To reguła najbardziej lewego prefiksu, a kandydaci, którzy ją wyjaśniają, pokazują, że rozumieją strukturę indeksu, a nie zapamiętane przepisy.
Jaka jest różnica między select_related a prefetch_related? select_related wykonuje SQL JOIN i działa dla relacji klucza obcego oraz jeden-do-jednego w pojedynczym zapytaniu. prefetch_related uruchamia drugie zapytanie i łączy wyniki w Pythonie, co jest wymagane dla relacji wiele-do-wielu i odwrotnych kluczy obcych. Sięgnięcie po niewłaściwą metodę albo pomija optymalizację, albo wywołuje niepotrzebne drugie zapytanie.
Czym różni się icontains od wyszukiwania pełnotekstowego na poziomie bazy danych? icontains kompiluje się do ILIKE '%term%', co nie może użyć standardowego indeksu B-tree i skanuje każdy wiersz. Wyszukiwanie pełnotekstowe dopasowuje do wstępnie policzonego tsvector wspartego indeksem GIN i rankinguje wyniki według trafności. To rozróżnienie jest wiarygodnym sygnałem, że kandydat wyszedł poza zabawkowe zbiory danych.
Dlaczego count() może być wolny na dużej tabeli i jakie są alternatywy? PostgreSQL nie przechowuje w pamięci podręcznej liczby wierszy tabeli, więc COUNT(*) skanuje każdy widoczny wiersz w modelu MVCC. Dla przybliżonych liczb zapytanie do pg_class.reltuples zwraca oszacowanie natychmiast, a dla stronicowania paginacja kluczowa (keyset) na indeksowanej kolumnie całkowicie unika liczenia. Uporządkowaną praktykę tych wzorców udostępnia moduł rekrutacyjny o cache w Django, gdzie liczniki oparte na cache są powracającym motywem, oraz cała ścieżka nauki Django.
Co ujawnia EXPLAIN ANALYZE, czego samo EXPLAIN nie pokazuje? EXPLAIN wypisuje szacowany koszt planera; EXPLAIN ANALYZE faktycznie wykonuje zapytanie i raportuje rzeczywiste czasy oraz liczby wierszy. Duża rozbieżność między szacowaną a rzeczywistą liczbą wierszy wskazuje na nieaktualne statystyki tabeli, naprawialne poleceniem ANALYZE, i jest częstą przyczyną tego, że planer ignoruje indeks, który powinien oczywiście zadziałać.
Zacznij ćwiczyć!
Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.
Podsumowanie
- Indeksy warto definiować w
Meta.indexeszamiastdb_index=True, aby indeksy złożone, częściowe i pokrywające znajdowały się w jednym, łatwym do audytu miejscu. - Kolumny indeksu złożonego należy porządkować według reguły najbardziej lewego prefiksu: kolumna wiodąca musi wystąpić w filtrze zapytania, aby indeks zadziałał.
- Indeksy częściowe sprawdzają się, gdy zapytania sięgają wyłącznie po mniejszość wierszy, na przykład zamówienia oczekujące w tabeli zdominowanej przez zakończoną historię.
- Indeksy pokrywające z
includeumożliwiają skany index-only i pomijają dostęp do sterty przy zapytaniach agregujących o dużej liczbie odczytów. - Wyszukiwanie przez
icontainswarto zastąpić przezSearchVector,SearchQueryiSearchRank, aby za darmo uzyskać ranking trafności oraz sprowadzanie do rdzeni. - Dokument wyszukiwania należy przechowywać w polu
SearchVectorFieldwspartym indeksemGinIndex, utrzymywanym w synchronizacji przez sygnał,GeneratedFieldlub wyzwalacz bazy danych, zanim wyszukiwanie pełnotekstowe trafi na skalę produkcyjną. - Decyzje o indeksach zawsze warto weryfikować poleceniem
EXPLAIN (ANALYZE, BUFFERS); indeks, którego planer nigdy nie wybiera, to podatek płacony przy zapisie bez korzyści przy odczycie.
Tagi
Udostępnij
Powiązane artykuły

Widoki asynchroniczne Django i ASGI w 2026: wydajność i pytania rekrutacyjne
Szczegółowe omówienie widoków asynchronicznych Django i ASGI w 2026 roku: jak działają od środka, który serwer wdrożyć, asynchroniczny ORM i pułapka SynchronousOnlyOperation oraz pytania rekrutacyjne.

Django 6.0 w 2026 roku: Klucze Kompozytowe, Zadania w Tle i Pytania Rekrutacyjne
Przewodnik po Django 6.0 w 2026: kompozytowe klucze glowne, wbudowany system zadan w tle, template partials, middleware CSP oraz pytania rekrutacyjne z przykladami kodu.

Django 5.2: Wlasne Middleware i Obsluga Sygnalow -- Przygotowanie do Rozmow Rekrutacyjnych
Praktyczny przewodnik po tworzeniu wlasnych middleware i obsludze sygnalow w Django 5.2. Struktura middleware, asynchroniczne middleware, sygnaly post_save i pre_save, wlasne sygnaly domenowe oraz najczesciej zadawane pytania rekrutacyjne.