Optimización del acceso a la base de datos

La capa de la base de datos de Django proporciona varias formas de ayudar a los desarrolladores a obtener el máximo rendimiento de sus bases de datos. Este documento reúne enlaces a la documentación relevante y agrega consejos variados, organizados bajo encabezados que resumen los pasos a seguir cuando se intenta optimizar su uso de la base de datos.

Perfil primero

Como práctica general de programación, esto no necesita decirse. Encuentra qué consultas estás haciendo y qué te están costando . Utilice el método explan de QuerySet.explain() para entender cómo se ejecutan específicas QuerySets por tu base de datos. También puede querer utilizar un proyecto externo como django-debug-toolbar, o una herramienta que monitoree directamente tu base de datos.

Recuerda que puedes estar optimizando para velocidad, memoria u ambos, dependiendo de tus requisitos. A veces optimizar para uno será perjudicial para el otro, pero a veces ayudarán entre sí. Además, el trabajo realizado por el proceso de la base de datos puede no tener el mismo costo (para ti) que el mismo trabajo realizado en tu proceso Python. Es tarea tuya decidir qué prioridades tienes, dónde debe estar el equilibrio y perfilar todo lo necesario ya que esto dependerá de tu aplicación y servidor.

Con todo lo que sigue, recuerda perfiles después de cada cambio para asegurarte de que el cambio es un beneficio y un beneficio suficiente dado la disminución en la legibilidad de tu código. Todos los consejos a continuación vienen con la advertencia de que en tus circunstancias la generalidad del principio no se aplica, o incluso puede ser revertido.

Utiliza técnicas estándar de optimización de bases de datos

…incluyendo:

  • Los índices. Esta es una prioridad número uno, después de que hayas determinado mediante perfilación qué índices deben agregarse. Utiliza Meta.indexes o Field.db_index para agregar estos desde Django. Considera agregar índices a campos que frecuentemente consultas utilizando filter(), exclude(), order_by(), etc., ya que los índices pueden ayudar a acelerar las búsquedas. Ten en cuenta que determinar los mejores índices es un tema complejo dependiente de la base de datos que dependerá de tu aplicación particular. El overhead del mantenimiento de un índice puede superar cualquier ganancia en velocidad de consulta.

  • Uso apropiado de tipos de campo.

Supondremos que has hecho las cosas enumeradas anteriormente. El resto de este documento se centra en cómo utilizar Django de manera que no estés haciendo trabajo innecesario. Este documento tampoco aborda otras técnicas de optimización que se aplican a todas las operaciones costosas, como caching general.

Entendimiento de QuerySets

Entender los QuerySets es vital para obtener una buena rendimiento con código simple. En particular:

Entendimiento de la evaluación de QuerySet

Para evitar problemas de rendimiento, es importante entender:

Entender atributos cacheados

Además del caché de todo el conjunto de consultas, hay un caché de los resultados de los atributos en objetos ORM. En general, se cachean los atributos que no sean llamables. Por ejemplo, suponiendo los modelos de blog example:

>>> entry = Entry.objects.get(id=1)
>>> entry.blog  # Blog object is retrieved at this point
>>> entry.blog  # cached version, no DB access

Sin embargo, en general, los atributos llamables causan consultas a la base de datos cada vez:

>>> entry = Entry.objects.get(id=1)
>>> entry.authors.all()  # query performed
>>> entry.authors.all()  # query performed again

Ten cuidado al leer el código de plantillas - el sistema de plantillas no permite el uso de paréntesis, pero llama automáticamente a las funciones llamables, ocultando la distinción anterior.

Ten cuidado con tus propias propiedades personalizadas - es responsabilidad tuya implementar el caché cuando sea necesario, por ejemplo utilizando el decorador cached_property.

Usa la etiqueta de plantilla with

Para aprovechar el comportamiento de caché del conjunto de consultas, es posible que debas usar la etiqueta de plantilla with.

Usa iterator()

Cuando tienes muchos objetos, el comportamiento de caché del conjunto de consultas puede causar un gran uso de memoria. En este caso, iterator() puede ayudarte.

Usa explain()

QuerySet.explain() te proporciona información detallada sobre cómo la base de datos ejecuta una consulta, incluyendo índices y joins que se utilizan. Estos detalles pueden ayudarte a encontrar consultas que podrían ser reescritas de manera más eficiente o identificar índices que podrían mejorar el rendimiento.

Haz trabajo en la base de datos en lugar de en Python

Por ejemplo:

Si estos no son suficientes para generar el SQL que necesitas:

Utiliza RawSQL

Un método menos portable pero más poderoso es la expresión RawSQL, que permite agregar explícitamente algunas sentencias SQL a la consulta. Si aún así no es lo suficientemente poderoso:

Utiliza SQL bruto

Escribe tu propio SQL personalizado para recuperar datos o poblar modelos. Utiliza django.db.connection.queries para saber qué SQL está escribiendo Django y comienza desde allí.

Recupera objetos individuales utilizando una columna única e indexada

Hay dos razones para utilizar una columna con unique o db_index cuando se utiliza get() para recuperar objetos individuales. Primero, la consulta será más rápida debido a el índice de base de datos subyacente. Además, la consulta podría tardar mucho más si coinciden múltiples objetos con la búsqueda; tener una restricción única en la columna garantiza que esto nunca sucederá.

Así utilizando los modelos de ejemplo de blog <queryset-model-example>:

>>> entry = Entry.objects.get(id=10)

será más rápido que:

>>> entry = Entry.objects.get(headline="News Item Title")

porque id está indexado por la base de datos y es garantizado ser único.

Hacer lo siguiente puede ser bastante lento:

>>> entry = Entry.objects.get(headline__startswith="News")

Primero, headline no está indexado, lo que hará que el fetch subyacente en la base de datos sea más lento.

Segundo, la búsqueda no garantiza que solo se devuelva un objeto. Si la consulta coincide con más de un objeto, se recuperarán y transferirán todos ellos desde la base de datos. Este penalización podría ser sustancial si se devuelven cientos o miles de registros. La penalización se comprenderá si la base de datos vive en un servidor separado, donde el overhead de red y latencia también juegan un papel.

Recupera todo a la vez si sabes que lo necesitarás

Hacer varias consultas al servidor de bases de datos para diferentes partes de un conjunto único de datos que necesitarás todas las partes es, en general, menos eficiente que recuperar todo en una sola consulta. Esto es particularmente importante si tienes una consulta que se ejecuta en un bucle y podría terminar haciendo muchas consultas a la base de datos, cuando solo era necesario hacer una. Por lo tanto:

No recuperes cosas que no necesitas

Usa QuerySet.values() y values_list()

Cuando solo deseas un dict o una list de valores y no necesitas objetos del modelo ORM, haz un uso apropiado de values(). Estos pueden ser útiles para reemplazar objetos del modelo en el código de plantilla - siempre que los dicts que proporciones tengan las mismas atributos que aquellos utilizados en la plantilla, estarás bien.

Usa QuerySet.defer() y only()

Usa defer() y only() si hay columnas de la base de datos que sabes que no necesitarás (o en la mayoría de los casos, no las necesitarás) para evitar cargarlas. Ten en cuenta que si lo haces, el ORM tendrá que ir a buscarlos en una consulta separada, lo que haría que esto fuera una pessimization si se utiliza inapropiadamente.

No seas demasiado agresivo al diferir campos sin perfilación ya que la base de datos tiene que leer la mayoría de los datos no de texto y no VARCHAR desde el disco para una sola fila en los resultados, incluso si solo se utiliza un par de columnas. Los métodos defer() y only() son más útiles cuando puedes evitar cargar mucha data de texto o campos que pueden requerir mucho procesamiento para convertirse nuevamente a Python. Como siempre, perfil primero, luego optimiza.

Utiliza QuerySet.contains(obj)

…si solo deseas saber si obj está en el conjunto de consultas, en lugar de if obj in queryset.

Utiliza QuerySet.count()

…si solo deseas la cuenta, en lugar de hacer len(queryset).

Utiliza QuerySet.exists()

…si solo deseas saber si existe al menos un resultado, en lugar de if queryset.

Pero:

No sobrecargues contains(), count() y exists()

Si vas a necesitar otros datos del QuerySet, evalúalo inmediatamente.

Los textos traducidos son:

members = group.members.all()

if display_group_members:
    if members:
        if current_user in members:
            print("You and", len(members) - 1, "other users are members of this group.")
        else:
            print("There are", len(members), "members in this group.")

        for member in members:
            print(member.username)
    else:
        print("There are no members in this group.")

Es óptimo porque:

  1. Dado que los QuerySets son perezosos, este no realiza consultas a la base de datos si display_group_members es False.

  2. Almacenar group.members.all() en la variable members permite reutilizar su resultado cache.

  3. La línea if members: causa que se llame a QuerySet.__bool__(), lo que provoca que se ejecute la consulta de group.members.all() en la base de datos. Si no hay resultados, devolverá False, de lo contrario True.

  4. La línea if current_user in members: verifica si el usuario está en el resultado cache, por lo que no se emiten consultas adicionales a la base de datos.

  5. El uso de len(members) llama a QuerySet.__len__(), reutilizando el resultado cache, por lo tanto, nuevamente, no se realizan consultas a la base de datos.

  6. El bucle for member itera sobre el resultado cache.

En total, este código realiza una o cero consultas a la base de datos. La única optimización deliberada realizada es utilizar la variable members. Utilizar QuerySet.exists() para el if, QuerySet.contains() para el in, o QuerySet.count() para el recuento provocaría cada uno consultas adicionales.

Utilice QuerySet.update() y delete()

Rather than retrieve a load of objects, set some values, and save them individual, use a bulk SQL UPDATE statement, via QuerySet.update(). De manera similar, utilice bulk deletes donde sea posible.

Ten en cuenta que estos métodos de actualización en masa no pueden llamar a los métodos save() o delete() de las instancias individuales, lo que significa que cualquier comportamiento personalizado que hayas agregado para estos métodos no se ejecutará, incluyendo cualquier cosa impulsada por la normal señal de objeto de base de datos señales.

Utilice directamente los valores de clave foránea

Si solo necesitas un valor de clave foránea, utilice el valor de clave foránea que ya está en el objeto que tienes, en lugar de obtener todo el objeto relacionado y tomar su clave primaria. Es decir, haga:

entry.blog_id

en lugar de:

entry.blog.id

No ordenar resultados si no importa

La ordenación no es gratuita; cada campo para ordenar por es una operación que debe realizar la base de datos. Si un modelo tiene una ordenación predeterminada (Meta.ordering) y no la necesita, elimínala en un QuerySet llamando a order_by() con ningún parámetro.

Agregar un índice a tu base de datos puede ayudar a mejorar el rendimiento de la ordenación.

Utilice métodos en masa

Utilice métodos en masa para reducir el número de sentencias SQL.

Crear en masa

Al crear objetos, donde sea posible, utilice el método bulk_create() para reducir el número de consultas SQL. Por ejemplo:

Entry.objects.bulk_create(
    [
        Entry(headline="This is a test"),
        Entry(headline="This is only a test"),
    ]
)

…es preferible a:

Entry.objects.create(headline="This is a test")
Entry.objects.create(headline="This is only a test")

Ten en cuenta que existen un número de caveatas con este método, así que asegúrese de que es apropiado para su caso de uso.

Actualizar en masa

Al actualizar objetos, donde sea posible, utilice el método bulk_update() para reducir el número de consultas SQL. Dado una lista o conjunto de objetos:

entries = Entry.objects.bulk_create(
    [
        Entry(headline="This is a test"),
        Entry(headline="This is only a test"),
    ]
)

El siguiente ejemplo:

entries[0].headline = "This is not a test"
entries[1].headline = "This is no longer a test"
Entry.objects.bulk_update(entries, ["headline"])

…es preferible a:

entries[0].headline = "This is not a test"
entries[0].save()
entries[1].headline = "This is no longer a test"
entries[1].save()

Ten en cuenta que existen un número de caveatas con este método, así que asegúrese de que es apropiado para su caso de uso.

Insertar en masa

Al insertar objetos en ManyToManyFields, utilice el método add() con múltiples objetos para reducir el número de consultas SQL. Por ejemplo:

my_band.members.add(me, my_friend)

…es preferible a:

my_band.members.add(me)
my_band.members.add(my_friend)

Donde Band y Artista son modelos con una relación muchos-a-muchos.

Cuando se insertan diferentes pares de objetos en ManyToManyField o cuando la tabla personalizada through está definida, utilice el método bulk_create() para reducir el número de consultas SQL. Por ejemplo:

PizzaToppingRelationship = Pizza.toppings.through
PizzaToppingRelationship.objects.bulk_create(
    [
        PizzaToppingRelationship(pizza=my_pizza, topping=pepperoni),
        PizzaToppingRelationship(pizza=your_pizza, topping=pepperoni),
        PizzaToppingRelationship(pizza=your_pizza, topping=mushroom),
    ],
    ignore_conflicts=True,
)

…es preferible a:

my_pizza.toppings.add(pepperoni)
your_pizza.toppings.add(pepperoni, mushroom)

…donde Pizza y Topping tienen una relación muchos-a-muchos. Ten en cuenta que hay un número de caveats a este método, así que asegúrate de que es apropiado para tu caso de uso.

Eliminar en masa

Cuando se eliminan objetos de ManyToManyFields, utilice remove() con múltiples objetos para reducir el número de consultas SQL. Por ejemplo:

my_band.members.remove(me, my_friend)

…es preferible a:

my_band.members.remove(me)
my_band.members.remove(my_friend)

Donde Band y Artista son modelos con una relación muchos-a-muchos.

Cuando se eliminan diferentes pares de objetos de ManyToManyFields, utilice delete() en una expresión Q con múltiples instancias del modelo through para reducir el número de consultas SQL. Por ejemplo:

from django.db.models import Q

PizzaToppingRelationship = Pizza.toppings.through
PizzaToppingRelationship.objects.filter(
    Q(pizza=my_pizza, topping=pepperoni)
    | Q(pizza=your_pizza, topping=pepperoni)
    | Q(pizza=your_pizza, topping=mushroom)
).delete()

…es preferible a:

my_pizza.toppings.remove(pepperoni)
your_pizza.toppings.remove(pepperoni, mushroom)

…donde Pizza y Topping tienen una relación muchos-a-muchos.