# Django ORM: optimizar las consultas para un rendimiento máximo > Guía completa para optimizar las consultas del ORM de Django. select_related, prefetch_related, índices, análisis del problema N+1 y técnicas avanzadas para aplicaciones de alto rendimiento. - Published: 2026-02-15 - Updated: 2026-04-28 - Author: SharpSkill - Tags: django, django orm, python, optimización base de datos, rendimiento - Reading time: 14 min --- El ORM de Django ofrece una abstracción elegante para interactuar con la base de datos, pero esa simplicidad puede ocultar problemas de rendimiento críticos. Una aplicación Django mal optimizada puede generar cientos de consultas allí donde una sola sería suficiente. Esta guía explora las técnicas esenciales para identificar y resolver esos problemas. > **La regla de oro de la optimización** > > Antes de optimizar, hay que medir. El uso de django-debug-toolbar en desarrollo permite visualizar cada consulta SQL generada e identificar rápidamente los cuellos de botella. ## Comprender el problema N+1 El problema N+1 representa la trampa más habitual al trabajar con ORM. Aparece cuando una consulta inicial recupera N objetos y, a continuación, se ejecutan N consultas adicionales para acceder a las relaciones de cada objeto. Esta multiplicación de consultas degrada gravemente el rendimiento. ```python # models.py from django.db import models class Author(models.Model): """Modelo que representa al autor de un libro.""" 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): """Modelo que representa un libro con su autor.""" 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 ``` Estos modelos sencillos permiten ilustrar el problema N+1 de forma concreta. ```python # views.py - Ejemplo problemático def list_books_bad(request): """❌ Esta vista genera N+1 consultas.""" books = Book.objects.all() # 1 consulta para los libros for book in books: # Cada acceso a book.author dispara una consulta SQL print(f"{book.title} por {book.author.name}") # ¡Con 100 libros = 101 consultas SQL! return render(request, 'books/list.html', {'books': books}) ``` Este código parece inocente pero genera una consulta por libro para recuperar al autor asociado. ## Resolver el N+1 con select_related El método `select_related` realiza un JOIN SQL y recupera los datos relacionados en una sola consulta. Funciona con relaciones ForeignKey y OneToOneField. ```python # views.py - Solución optimizada con select_related def list_books_optimized(request): """✅ Esta vista genera una única consulta con JOIN.""" # select_related realiza un JOIN SQL books = Book.objects.select_related('author').all() for book in books: # Sin consulta adicional: el autor ya está cargado print(f"{book.title} por {book.author.name}") # Total: 1 sola consulta SQL sin importar el número de libros return render(request, 'books/list.html', {'books': books}) ``` La consulta SQL generada utiliza un LEFT OUTER JOIN para recuperar los autores junto con los libros. ```python # Encadenar select_related para relaciones anidadas # 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): """Recupera libros, autores y editoriales en una sola consulta.""" books = Book.objects.select_related( 'author', # ForeignKey hacia Author 'publisher' # ForeignKey hacia Publisher ).all() return render(request, 'books/list.html', {'books': books}) ``` Varias relaciones pueden optimizarse al mismo tiempo enumerándolas en `select_related`. ## Optimizar las relaciones ManyToMany con prefetch_related Para las relaciones ManyToMany o las relaciones inversas (ForeignKey desde el otro lado), `prefetch_related` ejecuta consultas separadas pero optimizadas, evitando JOINs masivos. ```python # models.py class Tag(models.Model): """Etiquetas para categorizar los libros.""" 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') ``` La relación ManyToMany entre Book y Tag exige `prefetch_related` para una optimización eficaz. ```python # views.py - Optimización ManyToMany def list_books_with_tags(request): """✅ Recupera los libros y sus etiquetas de manera eficiente.""" books = Book.objects.prefetch_related('tags').all() for book in books: # Las etiquetas están precargadas, sin consulta adicional tag_names = [tag.name for tag in book.tags.all()] print(f"{book.title}: {', '.join(tag_names)}") # Total: 2 consultas (libros + etiquetas) sin importar la cantidad return render(request, 'books/list.html', {'books': books}) ``` `prefetch_related` ejecuta una consulta separada para las etiquetas y luego realiza la unión en Python. > **select_related vs prefetch_related** > > Utilizar `select_related` para ForeignKey y OneToOne (JOIN SQL). Utilizar `prefetch_related` para ManyToMany y relaciones inversas (consultas separadas). Ambos pueden combinarse en el mismo QuerySet. ## Personalizar la precarga con objetos Prefetch El objeto `Prefetch` permite un control fino sobre los datos precargados: filtrado, ordenación e incluso limitación del número de resultados. ```python # views.py from django.db.models import Prefetch def list_authors_with_recent_books(request): """Recupera los autores con solo sus libros recientes.""" # Prefetch personalizado: solo los libros desde 2025 recent_books_prefetch = Prefetch( 'books', queryset=Book.objects.filter( published_date__year__gte=2025 ).order_by('-published_date'), to_attr='recent_books' # Almacenado en un atributo personalizado ) authors = Author.objects.prefetch_related( recent_books_prefetch ).all() for author in authors: # Acceso a través del atributo personalizado for book in author.recent_books: print(f"{author.name}: {book.title}") return render(request, 'authors/list.html', {'authors': authors}) ``` El atributo `to_attr` guarda los resultados en una lista de Python en lugar del manager habitual. ```python # Combinación avanzada: select_related + Prefetch def list_authors_complete(request): """Ejemplo completo de optimización en varios niveles.""" authors = Author.objects.prefetch_related( Prefetch( 'books', queryset=Book.objects.select_related( 'publisher' # Optimiza también la editorial de cada libro ).prefetch_related( 'tags' # Y las etiquetas de cada libro ).filter(published_date__year=2026) ) ).all() return render(request, 'authors/complete.html', {'authors': authors}) ``` Este enfoque reduce drásticamente el número de consultas para estructuras de datos complejas. ## Usar only() y defer() para limitar las columnas De forma predeterminada, Django recupera todas las columnas de la tabla. Para modelos con muchos campos o campos muy grandes, limitar las columnas mejora el rendimiento. ```python # views.py def list_books_minimal(request): """Recupera solo las columnas necesarias.""" # only() especifica las columnas a incluir books = Book.objects.only( 'id', 'title', 'published_date' ).select_related('author') # Atención: acceder a un campo no incluido dispara una consulta for book in books: print(book.title) # OK, incluido # print(book.isbn) # Generaría una consulta adicional return render(request, 'books/list.html', {'books': books}) ``` El método `only()` crea un objeto «diferido» que carga únicamente las columnas indicadas. ```python # defer() para excluir columnas concretas def list_authors_without_bio(request): """Excluye campos grandes que rara vez se utilizan.""" # defer() excluye las columnas indicadas authors = Author.objects.defer( 'bio' # El TextField no se carga ).all() for author in authors: print(author.name) # OK print(author.email) # OK # author.bio cargaría el campo bajo demanda return render(request, 'authors/list.html', {'authors': authors}) ``` `defer()` es el inverso de `only()`: las columnas listadas no se cargan inicialmente. ## Optimizar con values() y values_list() Cuando solo se necesitan ciertos valores y no objetos de modelo completos, `values()` y `values_list()` devuelven diccionarios o tuplas más ligeros. ```python # views.py def get_book_titles(request): """Recupera solo los títulos como una lista.""" # values_list devuelve tuplas titles = Book.objects.values_list('title', flat=True) # Resultado: ['Libro 1', 'Libro 2', ...] # values devuelve diccionarios book_data = Book.objects.values('title', 'published_date') # Resultado: [{'title': 'Libro 1', 'published_date': ...}, ...] return render(request, 'books/titles.html', {'titles': titles}) ``` Estos métodos evitan la instanciación de objetos de modelo y reducen el consumo de memoria. ```python # Combinación con agregaciones from django.db.models import Count, Avg def get_author_statistics(request): """Estadísticas por autor sin cargar objetos completos.""" stats = Author.objects.values('name').annotate( book_count=Count('books'), avg_year=Avg('books__published_date__year') ).order_by('-book_count') # Resultado: [{'name': 'Autor', 'book_count': 5, 'avg_year': 2024}, ...] return render(request, 'authors/stats.html', {'stats': stats}) ``` La anotación permite realizar cálculos directamente en la base de datos. ## Crear índices para acelerar las consultas Los índices de la base de datos aceleran drásticamente las búsquedas. Django permite definirlos directamente en los modelos. ```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: # Índices compuestos para consultas frecuentes indexes = [ # Índice sobre la fecha de publicación (ordenación frecuente) models.Index( fields=['published_date'], name='book_pub_date_idx' ), # Índice compuesto para filtrar por autor + estado models.Index( fields=['author', 'status'], name='book_author_status_idx' ), # Índice parcial: solo libros disponibles models.Index( fields=['published_date'], name='book_available_idx', condition=models.Q(status='available') ), ] # Ordenación por defecto que aprovecha el índice ordering = ['-published_date'] ``` Estos índices mejoran el rendimiento de las consultas que filtran por esas columnas. > **Índices y operaciones de escritura** > > Los índices aceleran las lecturas pero ralentizan ligeramente las escrituras (INSERT, UPDATE). Conviene crearlos solo en columnas usadas con frecuencia en cláusulas WHERE, ORDER BY o JOIN. ## Recurrir a consultas SQL crudas cuando sea necesario Para consultas complejas u optimizaciones específicas del motor, las consultas SQL crudas ofrecen un control total. ```python # views.py from django.db import connection def get_books_with_raw_sql(request): """Consulta cruda para casos especiales.""" # Método 1: raw() para recuperar objetos del modelo 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): """Ejecución directa para consultas que no son SELECT.""" with connection.cursor() as cursor: # Consulta con agregación compleja 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}) ``` Las consultas crudas evitan el ORM pero sacrifican la portabilidad entre bases de datos. ## Analizar las consultas con django-debug-toolbar La herramienta django-debug-toolbar permite visualizar todas las consultas SQL generadas por una vista de Django. ```python # settings.py - Configuración de la debug toolbar INSTALLED_APPS = [ # ... otras aplicaciones 'debug_toolbar', ] MIDDLEWARE = [ 'debug_toolbar.middleware.DebugToolbarMiddleware', # ... otros middlewares ] # Mostrar la toolbar para peticiones locales INTERNAL_IPS = ['127.0.0.1'] DEBUG_TOOLBAR_PANELS = [ 'debug_toolbar.panels.sql.SQLPanel', # Consultas SQL 'debug_toolbar.panels.timer.TimerPanel', # Tiempo de ejecución 'debug_toolbar.panels.cache.CachePanel', # Caché ] ``` Esta configuración activa los paneles más útiles para optimizar. ```python # Registro de las consultas SQL en desarrollo # settings.py LOGGING = { 'version': 1, 'handlers': { 'console': { 'class': 'logging.StreamHandler', }, }, 'loggers': { 'django.db.backends': { 'level': 'DEBUG', 'handlers': ['console'], }, }, } ``` Este logging muestra cada consulta SQL en la consola, útil para detectar problemas N+1. ## Cachear los QuerySets Para los datos a los que se accede con frecuencia y se modifican poco, la caché evita consultas repetidas. ```python # views.py from django.core.cache import cache from django.views.decorators.cache import cache_page def list_featured_books(request): """Recupera los libros destacados con caché.""" cache_key = 'featured_books_list' # Intento de recuperación desde la caché books = cache.get(cache_key) if books is None: # Cache miss: ejecutar la consulta books = list( Book.objects.select_related('author') .filter(featured=True) .order_by('-published_date')[:10] ) # Guardar en caché durante 5 minutos cache.set(cache_key, books, timeout=300) return render(request, 'books/featured.html', {'books': books}) # Decorador para cachear toda la vista @cache_page(60 * 15) # Caché 15 minutos def list_all_tags(request): """Lista todas las etiquetas (datos que se modifican poco).""" tags = Tag.objects.annotate( book_count=Count('books') ).order_by('-book_count') return render(request, 'tags/list.html', {'tags': tags}) ``` La caché reduce la carga sobre la base de datos para datos estáticos. ## Conclusión La optimización de consultas con el ORM de Django se apoya en unos cuantos principios fundamentales: ✅ **Medir antes de optimizar**: utilizar django-debug-toolbar para detectar problemas reales ✅ **Eliminar el N+1**: `select_related` para ForeignKey, `prefetch_related` para ManyToMany ✅ **Limitar los datos**: `only()`, `defer()`, `values()` para cargar solo lo necesario ✅ **Indexar con criterio**: crear índices en columnas filtradas u ordenadas con frecuencia ✅ **Cachear de forma estratégica**: los datos que cambian poco se benefician de la caché de Django ✅ **Consultas crudas**: como último recurso para optimizaciones específicas del motor Aplicadas con método, estas técnicas transforman una aplicación Django lenta en un sistema eficiente capaz de gestionar grandes volúmenes de datos. El ORM de Django sigue siendo una herramienta potente cuando se dominan sus matices. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/es/blog/django/django-orm-optimizing-queries