Django y PostgreSQL en 2026: indexación, búsqueda de texto completo y preguntas de entrevista
Guía práctica de optimización de Django con PostgreSQL: índices B-tree, parciales y de cobertura, búsqueda de texto completo con SearchVector y GIN, además de preguntas de entrevista 2026.

La optimización de Django con PostgreSQL marca la diferencia entre una aplicación que escala más allá de sus primeros mil usuarios y otra que se paraliza en cada vista de lista. PostgreSQL incluye un motor de indexación y un subsistema de búsqueda de texto completo que la mayoría de los proyectos Django deja sin tocar, desperdiciando un rendimiento de consultas que no cuesta nada recuperar. Esta guía cubre los patrones de indexación, las herramientas de búsqueda de django.contrib.postgres y las preguntas de entrevista sobre bases de datos que separan a los ingenieros Django de nivel medio de los senior en 2026.
Cada índice acelera las lecturas, pero ralentiza las escrituras y consume disco. Conviene ejecutar EXPLAIN (ANALYZE, BUFFERS) sobre la consulta real, agregar el índice y luego comparar el plan. Un índice que el planificador de consultas nunca elige es pura sobrecarga en cada INSERT y UPDATE.
Fundamentos de indexación de PostgreSQL en Django
Por defecto, PostgreSQL crea un índice B-tree solo en las claves primarias y en las columnas con unique=True. Cualquier otra columna filtrada u ordenada ejecuta un escaneo secuencial hasta que exista un índice. Django expone los índices a través de Meta.indexes, que se prefiere sobre el antiguo db_index=True porque admite índices compuestos, parciales y de cobertura en un solo lugar.
El orden de las columnas dentro de un índice compuesto no es cosmético. Un B-tree puede usar un índice para una consulta solo cuando el filtro utiliza un prefijo inicial de las columnas indexadas, una regla explicada en profundidad en Use The Index, Luke. Un índice sobre (status, created_at) sirve para WHERE status = ? ORDER BY created_at, pero ese mismo índice no puede acelerar una consulta que filtra únicamente por 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 = [
# B-tree de una columna para búsquedas exactas y por rango en status
models.Index(fields=["status"]),
# Índice compuesto: sirve para WHERE status = ? ORDER BY created_at DESC
# La columna inicial (status) debe aparecer en el filtro para poder usarse
models.Index(fields=["status", "-created_at"]),
]El índice compuesto convierte una consulta de tablero como "últimos pedidos pendientes" en un único escaneo de índice, en lugar de un filtro seguido de una ordenación sobre toda la tabla.
Índices parciales y de cobertura para consultas dirigidas
Un índice parcial cubre solo las filas que cumplen una condición, por lo que se mantiene pequeño y reside en memoria. Cuando la mayoría de las filas comparten un valor y las consultas solo tocan la minoría, un índice parcial resulta mucho más económico que uno completo. Un índice de cobertura va más allá: al agregar columnas que no son clave con include, PostgreSQL responde la consulta directamente desde el índice sin tocar el heap de la tabla, un patrón de acceso llamado escaneo solo de índice (index-only scan).
# models.py
from django.db import models
from django.db.models import Q
class Order(models.Model):
# campos definidos arriba
class Meta:
indexes = [
# Índice parcial: solo indexa las filas pendientes, ignora el historial completado
models.Index(
fields=["created_at"],
name="pending_orders_idx",
condition=Q(status="pending"),
),
# Índice de cobertura: total se lee directo del índice (index-only scan)
models.Index(
fields=["status"],
include=["total"],
name="status_total_covering_idx",
),
]En una tabla de pedidos donde el 2% de las filas está en estado pendiente, el índice parcial puede ser cincuenta veces más pequeño que un índice completo sobre la misma columna, y el planificador lo mantiene caliente en la caché de búferes. La cláusula include está documentada en los escaneos solo de índice de PostgreSQL y es la mejora más pasada por alto en las aplicaciones Django que agregan una columna numérica detrás de un filtro de estado.
Búsqueda de texto completo en Django con PostgreSQL
Recurrir a title__icontains=query resulta cómodo, pero fuerza un escaneo secuencial y no puede clasificar los resultados por relevancia. La búsqueda de texto completo de Django envuelve los tipos nativos tsvector y tsquery de PostgreSQL a través de SearchVector, SearchQuery y SearchRank. Un vector de búsqueda normaliza el texto en lexemas, elimina las palabras vacías y reduce las palabras a su raíz, de modo que una búsqueda de "running" coincide con "run" y "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")
# El peso A clasifica las coincidencias del título por encima del cuerpo (peso 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")
)El parámetro weight asigna niveles de prioridad de A (el más alto) a D. Una coincidencia en el título se clasifica por encima de una coincidencia en el cuerpo aunque ambos contengan el término buscado, lo que refleja cómo los usuarios esperan que se comporte una búsqueda. La API completa y el modelo de ponderación de cuatro niveles se detallan en la referencia de búsqueda de texto completo de Django.
Índices GIN y SearchVectorField para búsquedas rápidas
La consulta anterior recalcula el vector de búsqueda para cada fila en cada solicitud, lo cual es aceptable para unos miles de filas e inaceptable para un millón. El patrón de producción almacena el vector en un SearchVectorField y lo indexa con un índice GIN, el tipo de índice que PostgreSQL usa para valores compuestos como los documentos de texto completo y 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()
# Documento de búsqueda almacenado y precalculado
search_vector = SearchVectorField(null=True)
class Meta:
indexes = [
# GIN convierte las búsquedas de texto completo en escaneos logarítmicos
GinIndex(fields=["search_vector"]),
]El campo almacenado debe mantenerse sincronizado con title y body. Una señal post_save que escribe el vector con .update() evita disparar la señal de forma recursiva, ya que QuerySet.update() no emite señales de guardado.
# 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"),
)Para los equipos que usan Django 5.0 o una versión más reciente, un GeneratedField calculado por la base de datos o un disparador de PostgreSQL elimina por completo el UPDATE adicional al mantener la columna dentro de la base de datos. Sea cual sea el mecanismo que la mantenga poblada, la consulta filtra entonces el campo indexado directamente, y el índice GIN convierte lo que era un escaneo completo de tabla en una búsqueda logarítmica.
Los índices GIN aceleran las lecturas con un costo real de escritura, ya que cada inserción toca muchas entradas del índice. En tablas con muchas escrituras, conviene configurar el parámetro de almacenamiento fastupdate y vigilar el tamaño de la lista pendiente, o las escrituras de texto completo se estancarán detrás del mantenimiento del índice.
Cómo elegir el tipo de índice adecuado en PostgreSQL
B-tree es el valor predeterminado correcto, pero PostgreSQL ofrece varios tipos de índice a través de django.contrib.postgres.indexes, y elegir el equivocado desperdicia disco sin mejorar una sola consulta. La elección depende de la forma de los datos y del operador que usa la consulta, tal como se cataloga en la documentación de tipos de índice de PostgreSQL.
| Tipo de índice | Ideal para | Clase de Django |
|------------|----------|--------------|
| B-tree | Igualdad y rango en columnas escalares | models.Index |
| GIN | Búsqueda de texto completo, JSONB, contención en arreglos | GinIndex |
| BRIN | Tablas enormes de solo anexado ordenadas por una columna | BrinIndex |
| Hash | Búsquedas solo por igualdad en columnas grandes | HashIndex |
BRIN merece atención especial para los datos de series temporales. En una tabla de decenas de millones de filas insertadas en orden de marca de tiempo, un BrinIndex sobre created_at ocupa unos pocos kilobytes donde un B-tree necesitaría cientos de megabytes, porque BRIN almacena solo el valor mínimo y máximo por rango de bloques. La contrapartida es que BRIN solo ayuda cuando el orden físico de las filas sigue a la columna indexada, algo que se cumple naturalmente en los registros de solo anexado y las tablas de eventos.
# 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 = [
# Índice diminuto para escaneos por rango en una tabla de solo anexado y orden temporal
BrinIndex(fields=["created_at"]),
]Ajustar el tipo de índice al patrón de acceso es un tema recurrente en las entrevistas senior, porque demuestra que el candidato razona sobre la disposición del almacenamiento en lugar de agregar B-trees por reflejo en todas partes.
¿Listo para aprobar tus entrevistas de Django?
Practica con nuestros simuladores interactivos, flashcards y tests técnicos.
Preguntas de entrevista sobre bases de datos en Django en 2026
Las preguntas sobre bases de datos dominan las entrevistas senior de Django porque la fluidez con el ORM no implica entender lo que este emite. Las preguntas siguientes reflejan lo que los paneles de contratación realmente indagan en 2026, y encajan de forma natural con el ajuste a nivel de consulta que cubre la guía de optimización de consultas del ORM de Django.
¿Cuándo un índice compuesto sobre (a, b) no ayuda a una consulta? Cuando la consulta no filtra por a. Un B-tree se ordena primero por su columna inicial, de modo que un filtro solo sobre b no puede usar el índice. Esta es la regla del prefijo más a la izquierda, y los candidatos que la explican demuestran que entienden la estructura del índice en lugar de recetas memorizadas.
¿Cuál es la diferencia entre select_related y prefetch_related? select_related realiza un JOIN de SQL y funciona para relaciones de clave foránea y uno a uno en una sola consulta. prefetch_related ejecuta una segunda consulta y une los resultados en Python, lo cual es necesario para las relaciones muchos a muchos y de clave foránea inversa. Recurrir al equivocado o pierde la optimización o dispara una segunda consulta innecesaria.
¿En qué se diferencia icontains de la búsqueda de texto completo a nivel de base de datos? icontains se compila a ILIKE '%term%', que no puede usar un índice B-tree estándar y escanea todas las filas. La búsqueda de texto completo coincide contra un tsvector precalculado respaldado por un índice GIN y clasifica los resultados por relevancia. La distinción es una señal confiable de que un candidato ha superado los conjuntos de datos de juguete.
¿Por qué count() puede ser lento en una tabla grande y cuáles son las alternativas? PostgreSQL no tiene un conteo de filas en caché para una tabla, así que COUNT(*) escanea todas las filas visibles bajo MVCC. Para conteos aproximados, consultar pg_class.reltuples devuelve una estimación al instante, y para la paginación, la paginación por clave (keyset) sobre una columna indexada evita el conteo por completo. Hay práctica estructurada sobre estos patrones en el módulo de entrevista sobre caché de Django, donde los contadores respaldados por caché son un tema recurrente, y en toda la ruta de aprendizaje de Django.
¿Qué revela EXPLAIN ANALYZE que EXPLAIN por sí solo no muestra? EXPLAIN imprime el costo estimado del planificador; EXPLAIN ANALYZE ejecuta realmente la consulta e informa los tiempos y conteos de filas reales. Una gran brecha entre las filas estimadas y las reales apunta a estadísticas de tabla desactualizadas, corregibles con ANALYZE, y es una causa raíz común de que el planificador ignore un índice que obviamente debería aplicar.
¡Empieza a practicar!
Pon a prueba tu conocimiento con nuestros simuladores de entrevista y tests técnicos.
Conclusión
- Definir los índices en
Meta.indexesen lugar dedb_index=Truepara que los índices compuestos, parciales y de cobertura vivan en un solo lugar auditable. - Ordenar las columnas del índice compuesto según la regla del prefijo más a la izquierda: la columna inicial debe aparecer en el filtro de la consulta para que el índice se aplique.
- Usar índices parciales cuando las consultas solo tocan una minoría de filas, como los pedidos pendientes en una tabla dominada por el historial completado.
- Agregar índices de cobertura con
includepara habilitar los escaneos solo de índice y evitar el acceso al heap en consultas de agregación con muchas lecturas. - Reemplazar la búsqueda con
icontainsporSearchVector,SearchQueryySearchRankpara obtener clasificación por relevancia y derivación de raíces sin esfuerzo. - Almacenar el documento de búsqueda en un
SearchVectorFieldrespaldado por unGinIndex, mantenido en sincronía por una señal, unGeneratedFieldo un disparador de base de datos, antes de que la búsqueda de texto completo alcance la escala de producción. - Validar siempre las decisiones de indexación con
EXPLAIN (ANALYZE, BUFFERS); un índice que el planificador nunca selecciona es un impuesto en tiempo de escritura sin beneficio en tiempo de lectura.
Etiquetas
Compartir
Artículos relacionados

Vistas asíncronas de Django y ASGI en 2026: rendimiento y preguntas de entrevista
Un análisis a fondo de las vistas asíncronas de Django y ASGI en 2026: cómo funcionan por dentro, qué servidor desplegar, el ORM asíncrono y la trampa de SynchronousOnlyOperation, además de preguntas de entrevista.

Django 6.0 en 2026: Claves Primarias Compuestas, Tareas en Segundo Plano y Preguntas de Entrevista Técnica
Guía completa de Django 6.0: claves primarias compuestas, tareas nativas en segundo plano, template partials, middleware CSP y preguntas de entrevista.

Django y Celery en 2026: Procesamiento Asíncrono y Preguntas de Entrevista
Guía completa de Django y Celery en 2026: configuración, colas, tareas periódicas, monitoreo con Flower, comparativa con Django 6.0 y preguntas de entrevista técnica.