# Django ORM: optymalizacja zapytań dla maksymalnej wydajności > Kompletny przewodnik po optymalizacji zapytań Django ORM. select_related, prefetch_related, indeksy, analiza problemu N+1 oraz zaawansowane techniki dla aplikacji o wysokiej wydajności. - Published: 2026-02-15 - Updated: 2026-04-28 - Author: SharpSkill - Tags: django, django orm, python, optymalizacja bazy danych, wydajność - Reading time: 14 min --- Django ORM oferuje eleganckie abstrakcje do pracy z bazą danych, jednak ta prostota potrafi maskować krytyczne problemy z wydajnością. Źle zoptymalizowana aplikacja Django może wygenerować setki zapytań tam, gdzie wystarczyłoby jedno. Niniejszy przewodnik prezentuje podstawowe techniki służące do identyfikacji i rozwiązywania takich problemów. > **Złota zasada optymalizacji** > > Zanim cokolwiek zostanie zoptymalizowane, należy najpierw zmierzyć. Korzystanie z django-debug-toolbar w środowisku deweloperskim pozwala podejrzeć każde wygenerowane zapytanie SQL i szybko zlokalizować wąskie gardła. ## Zrozumienie problemu N+1 Problem N+1 to najczęstsza pułapka związana z ORM. Występuje wtedy, gdy zapytanie początkowe pobiera N obiektów, a następnie wykonywanych jest N dodatkowych zapytań, by uzyskać dostęp do relacji każdego z nich. Takie mnożenie zapytań drastycznie pogarsza wydajność. ```python # models.py from django.db import models class Author(models.Model): """Model reprezentujący autora książki.""" name = models.CharField(max_length=200) email = models.EmailField(unique=True) bio = models.TextField(blank=True) def __str__(self): return self.name class Book(models.Model): """Model reprezentujący książkę wraz z autorem.""" title = models.CharField(max_length=300) author = models.ForeignKey( Author, on_delete=models.CASCADE, related_name='books' ) published_date = models.DateField() isbn = models.CharField(max_length=13, unique=True) def __str__(self): return self.title ``` Te proste modele pozwalają zilustrować problem N+1 w sposób konkretny. ```python # views.py - Przykład problematyczny def list_books_bad(request): """❌ Ten widok generuje N+1 zapytań.""" books = Book.objects.all() # 1 zapytanie po książki for book in books: # Każdy dostęp do book.author wyzwala zapytanie SQL print(f"{book.title} autorstwa {book.author.name}") # Przy 100 książkach = 101 zapytań SQL! return render(request, 'books/list.html', {'books': books}) ``` Kod wygląda niewinnie, ale na każdą książkę generuje zapytanie pobierające jej autora. ## Rozwiązanie problemu N+1 za pomocą select_related Metoda `select_related` wykonuje JOIN SQL i pobiera powiązane dane w jednym zapytaniu. Działa dla relacji ForeignKey i OneToOneField. ```python # views.py - Rozwiązanie zoptymalizowane z select_related def list_books_optimized(request): """✅ Ten widok generuje pojedyncze zapytanie z JOIN.""" # select_related wykonuje JOIN SQL books = Book.objects.select_related('author').all() for book in books: # Brak dodatkowych zapytań: autor jest już załadowany print(f"{book.title} autorstwa {book.author.name}") # Łącznie: 1 zapytanie SQL niezależnie od liczby książek return render(request, 'books/list.html', {'books': books}) ``` Wygenerowane zapytanie SQL korzysta z LEFT OUTER JOIN, by pobrać autorów razem z książkami. ```python # Łączenie select_related dla relacji zagnieżdżonych # models.py class Publisher(models.Model): name = models.CharField(max_length=200) country = models.CharField(max_length=100) class Book(models.Model): title = models.CharField(max_length=300) author = models.ForeignKey(Author, on_delete=models.CASCADE) publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE) # views.py def list_books_with_details(request): """Pobiera książki, autorów i wydawców w jednym zapytaniu.""" books = Book.objects.select_related( 'author', # ForeignKey do Author 'publisher' # ForeignKey do Publisher ).all() return render(request, 'books/list.html', {'books': books}) ``` Kilka relacji można optymalizować jednocześnie, wymieniając je w `select_related`. ## Optymalizacja relacji ManyToMany za pomocą prefetch_related W przypadku relacji ManyToMany lub relacji odwrotnych (ForeignKey z drugiej strony) `prefetch_related` wykonuje osobne, ale zoptymalizowane zapytania, unikając ogromnych JOIN-ów. ```python # models.py class Tag(models.Model): """Tagi do kategoryzowania książek.""" name = models.CharField(max_length=50, unique=True) def __str__(self): return self.name class Book(models.Model): title = models.CharField(max_length=300) author = models.ForeignKey(Author, on_delete=models.CASCADE) tags = models.ManyToManyField(Tag, related_name='books') ``` Relacja ManyToMany między Book i Tag wymaga `prefetch_related`, aby skutecznie ją zoptymalizować. ```python # views.py - Optymalizacja ManyToMany def list_books_with_tags(request): """✅ Pobiera książki i ich tagi w sposób efektywny.""" books = Book.objects.prefetch_related('tags').all() for book in books: # Tagi są wstępnie załadowane, brak dodatkowych zapytań tag_names = [tag.name for tag in book.tags.all()] print(f"{book.title}: {', '.join(tag_names)}") # Łącznie: 2 zapytania (książki + tagi) niezależnie od liczby return render(request, 'books/list.html', {'books': books}) ``` `prefetch_related` wykonuje osobne zapytanie po tagi, a następnie łączy dane w Pythonie. > **select_related vs prefetch_related** > > Dla ForeignKey i OneToOne należy używać `select_related` (JOIN SQL). Dla ManyToMany i relacji odwrotnych warto sięgnąć po `prefetch_related` (osobne zapytania). Obie metody można łączyć w tym samym QuerySecie. ## Personalizacja wstępnego ładowania za pomocą obiektów Prefetch Obiekt `Prefetch` umożliwia szczegółową kontrolę nad wstępnie ładowanymi danymi: filtrowanie, sortowanie, a nawet ograniczanie liczby wyników. ```python # views.py from django.db.models import Prefetch def list_authors_with_recent_books(request): """Pobiera autorów wyłącznie z ich nowymi książkami.""" # Prefetch niestandardowy: tylko książki od 2025 roku recent_books_prefetch = Prefetch( 'books', queryset=Book.objects.filter( published_date__year__gte=2025 ).order_by('-published_date'), to_attr='recent_books' # Przechowywane w atrybucie niestandardowym ) authors = Author.objects.prefetch_related( recent_books_prefetch ).all() for author in authors: # Dostęp przez atrybut niestandardowy for book in author.recent_books: print(f"{author.name}: {book.title}") return render(request, 'authors/list.html', {'authors': authors}) ``` Atrybut `to_attr` zapisuje wyniki w liście Pythona zamiast w typowym managerze. ```python # Zaawansowane połączenie: select_related + Prefetch def list_authors_complete(request): """Kompletny przykład wielopoziomowej optymalizacji.""" authors = Author.objects.prefetch_related( Prefetch( 'books', queryset=Book.objects.select_related( 'publisher' # Optymalizuje również wydawcę każdej książki ).prefetch_related( 'tags' # Oraz tagi każdej książki ).filter(published_date__year=2026) ) ).all() return render(request, 'authors/complete.html', {'authors': authors}) ``` Takie podejście drastycznie zmniejsza liczbę zapytań przy złożonych strukturach danych. ## Ograniczanie kolumn za pomocą only() i defer() Domyślnie Django pobiera wszystkie kolumny tabeli. W modelach z wieloma polami lub dużymi polami ograniczenie kolumn poprawia wydajność. ```python # views.py def list_books_minimal(request): """Pobiera tylko niezbędne kolumny.""" # only() określa kolumny do uwzględnienia books = Book.objects.only( 'id', 'title', 'published_date' ).select_related('author') # Uwaga: dostęp do pola spoza listy wyzwala zapytanie for book in books: print(book.title) # OK, uwzględnione # print(book.isbn) # Spowodowałoby dodatkowe zapytanie return render(request, 'books/list.html', {'books': books}) ``` Metoda `only()` tworzy obiekt „deferred”, który ładuje wyłącznie wskazane kolumny. ```python # defer() do wykluczania konkretnych kolumn def list_authors_without_bio(request): """Pomija duże pola używane sporadycznie.""" # defer() wyklucza wskazane kolumny authors = Author.objects.defer( 'bio' # TextField nie jest ładowany ).all() for author in authors: print(author.name) # OK print(author.email) # OK # author.bio załadowałoby pole na żądanie return render(request, 'authors/list.html', {'authors': authors}) ``` `defer()` jest odwrotnością `only()`: wymienione kolumny nie są ładowane na początku. ## Optymalizacja za pomocą values() i values_list() Gdy potrzebne są jedynie wybrane wartości bez pełnych obiektów modelu, `values()` i `values_list()` zwracają lżejsze słowniki lub krotki. ```python # views.py def get_book_titles(request): """Pobiera same tytuły jako listę.""" # values_list zwraca krotki titles = Book.objects.values_list('title', flat=True) # Wynik: ['Książka 1', 'Książka 2', ...] # values zwraca słowniki book_data = Book.objects.values('title', 'published_date') # Wynik: [{'title': 'Książka 1', 'published_date': ...}, ...] return render(request, 'books/titles.html', {'titles': titles}) ``` Te metody pozwalają uniknąć tworzenia obiektów modelu i zmniejszają zużycie pamięci. ```python # Połączenie z agregacjami from django.db.models import Count, Avg def get_author_statistics(request): """Statystyki na autora bez ładowania obiektów.""" stats = Author.objects.values('name').annotate( book_count=Count('books'), avg_year=Avg('books__published_date__year') ).order_by('-book_count') # Wynik: [{'name': 'Autor', 'book_count': 5, 'avg_year': 2024}, ...] return render(request, 'authors/stats.html', {'stats': stats}) ``` Adnotacje pozwalają wykonać obliczenia bezpośrednio w bazie danych. ## Tworzenie indeksów przyspieszających zapytania Indeksy bazodanowe drastycznie przyspieszają wyszukiwanie. Django pozwala definiować je bezpośrednio w modelach. ```python # models.py from django.db import models class Book(models.Model): title = models.CharField(max_length=300, db_index=True) author = models.ForeignKey(Author, on_delete=models.CASCADE) published_date = models.DateField() isbn = models.CharField(max_length=13, unique=True) status = models.CharField(max_length=20, default='available') class Meta: # Indeksy złożone dla częstych zapytań indexes = [ # Indeks na dacie publikacji (częste sortowanie) models.Index( fields=['published_date'], name='book_pub_date_idx' ), # Indeks złożony do filtrowania po autorze i statusie models.Index( fields=['author', 'status'], name='book_author_status_idx' ), # Indeks częściowy: tylko książki dostępne models.Index( fields=['published_date'], name='book_available_idx', condition=models.Q(status='available') ), ] # Domyślne sortowanie korzystające z indeksu ordering = ['-published_date'] ``` Te indeksy poprawiają wydajność zapytań filtrujących po wskazanych kolumnach. > **Indeksy a operacje zapisu** > > Indeksy przyspieszają odczyty, ale nieznacznie spowalniają operacje zapisu (INSERT, UPDATE). Należy je tworzyć tylko na kolumnach często używanych w klauzulach WHERE, ORDER BY lub JOIN. ## Sięganie po surowe zapytania SQL w razie potrzeby Dla zapytań złożonych lub optymalizacji specyficznych dla danej bazy surowe zapytania SQL dają pełną kontrolę. ```python # views.py from django.db import connection def get_books_with_raw_sql(request): """Surowe zapytanie do szczególnych przypadków.""" # Metoda 1: raw() do pobrania obiektów modelu books = Book.objects.raw(''' SELECT b.*, a.name as author_name FROM library_book b INNER JOIN library_author a ON b.author_id = a.id WHERE b.published_date > %s ORDER BY b.published_date DESC ''', ['2025-01-01']) return render(request, 'books/list.html', {'books': books}) def execute_custom_query(request): """Bezpośrednie wykonanie dla zapytań innych niż SELECT.""" with connection.cursor() as cursor: # Zapytanie ze złożoną agregacją cursor.execute(''' SELECT a.name, COUNT(b.id) as book_count, AVG(EXTRACT(YEAR FROM b.published_date)) as avg_year FROM library_author a LEFT JOIN library_book b ON b.author_id = a.id GROUP BY a.id, a.name HAVING COUNT(b.id) > 2 ORDER BY book_count DESC ''') results = cursor.fetchall() return render(request, 'stats.html', {'results': results}) ``` Surowe zapytania omijają ORM, ale tracą przenośność między bazami danych. ## Analiza zapytań z django-debug-toolbar Narzędzie django-debug-toolbar pozwala zobaczyć wszystkie zapytania SQL generowane przez widok Django. ```python # settings.py - Konfiguracja debug toolbara INSTALLED_APPS = [ # ... pozostałe aplikacje 'debug_toolbar', ] MIDDLEWARE = [ 'debug_toolbar.middleware.DebugToolbarMiddleware', # ... pozostałe middleware ] # Pokaż toolbar dla żądań lokalnych INTERNAL_IPS = ['127.0.0.1'] DEBUG_TOOLBAR_PANELS = [ 'debug_toolbar.panels.sql.SQLPanel', # Zapytania SQL 'debug_toolbar.panels.timer.TimerPanel', # Czas wykonania 'debug_toolbar.panels.cache.CachePanel', # Cache ] ``` Taka konfiguracja włącza panele najbardziej przydatne do optymalizacji. ```python # Logowanie zapytań SQL podczas dewelopmentu # settings.py LOGGING = { 'version': 1, 'handlers': { 'console': { 'class': 'logging.StreamHandler', }, }, 'loggers': { 'django.db.backends': { 'level': 'DEBUG', 'handlers': ['console'], }, }, } ``` Takie logowanie wyświetla każde zapytanie SQL w konsoli, co pomaga wykrywać problemy N+1. ## Cache'owanie QuerySetów Dla danych pobieranych często, a rzadko modyfikowanych, cache pozwala uniknąć powtarzających się zapytań. ```python # views.py from django.core.cache import cache from django.views.decorators.cache import cache_page def list_featured_books(request): """Pobiera wyróżnione książki z wykorzystaniem cache.""" cache_key = 'featured_books_list' # Próba odczytu z cache books = cache.get(cache_key) if books is None: # Cache miss: wykonaj zapytanie books = list( Book.objects.select_related('author') .filter(featured=True) .order_by('-published_date')[:10] ) # Zapisz w cache na 5 minut cache.set(cache_key, books, timeout=300) return render(request, 'books/featured.html', {'books': books}) # Dekorator cache'ujący cały widok @cache_page(60 * 15) # Cache 15 minut def list_all_tags(request): """Lista wszystkich tagów (rzadko modyfikowane dane).""" tags = Tag.objects.annotate( book_count=Count('books') ).order_by('-book_count') return render(request, 'tags/list.html', {'tags': tags}) ``` Cache zmniejsza obciążenie bazy danych dla danych statycznych. ## Podsumowanie Optymalizacja zapytań Django ORM opiera się na kilku podstawowych zasadach: ✅ **Mierz przed optymalizacją**: korzystaj z django-debug-toolbar, by wykrywać realne problemy ✅ **Eliminuj problem N+1**: `select_related` dla ForeignKey, `prefetch_related` dla ManyToMany ✅ **Ograniczaj dane**: `only()`, `defer()`, `values()` ładują tylko to, co potrzebne ✅ **Indeksuj rozsądnie**: zakładaj indeksy na kolumnach często filtrowanych lub sortowanych ✅ **Cache strategiczny**: dane rzadko modyfikowane zyskują na cache'u Django ✅ **Surowe zapytania**: jako ostateczność dla optymalizacji specyficznych dla bazy Stosowane metodycznie, te techniki przekształcają wolną aplikację Django w wydajny system zdolny obsłużyć duże wolumeny danych. Django ORM pozostaje potężnym narzędziem, gdy opanowane zostaną jego niuanse. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/pl/blog/django/django-orm-optimizing-queries