django+keyword

Introducción

Tal como expusimos en el primer post de la serie, durante el desarrollo de los proyectos los programadores tenemos que afrontar situaciones donde el rendimiento es un tema capital.

Tenemos que tener presente que en esta serie de posts vamos a analizar una serie de problemas clásicos, que se pueden encontrar en los proyectos y que si los conocemos de antemano nos puede ahorrar muchos quebraderos de cabeza.

Como podréis recordar del primer post, las situaciones que estamos analizando son las siguientes:

  • Gran cantidad de datos sin filtrar.
  • Devolver más información de la necesaria de las tablas.
  • N + 1 queries - JOINS.
  • Reducir queries de escritura: detalles de implementación.
  • Índices: declararlos y asegurarse de que se utilizan.
  • Queries complejas.

En este segundo post, planteamos el problema de estar consultando más información de la necesaria a la base de datos. Este ejercicio aumenta ligeramente la complejidad afrontada, pero la resolución sigue siendo de nivel básico.

Modelo de datos

Como vimos en el primer post, vamos a utilizar el mismo modelo de datos para el proyecto del pasaporte COVID, diseñado como referencia.

Modelo datos

En este caso nos vamos a centrar en la entidad Person, donde se decidió almacenar la información de los usuarios de la aplicación. El modelo django Person es el siguiente:

class Person(models.Model):
   created_at = models.DateTimeField(verbose_name="Creation date", auto_now_add=True)
   updated_at = models.DateTimeField(verbose_name="Update date", auto_now_add=True)
   name = models.CharField(max_length=100)
   surnames = models.CharField(max_length=150)
   passport = models.CharField(max_length=10, unique=True)
   phone = models.CharField(max_length=20)
   email = models.CharField(max_length=100, blank=True)
   country = models.ForeignKey(Country, on_delete=models.PROTECT)
   contacts = models.ManyToManyField("self", through="Contacts", blank=True)

El problema

El problema que planteamos para este post es que queremos recuperar los datos de los ciudadanos que utilizan el pasaporte europeo, pero granulando por diferentes subconjuntos de datos. Ya bien sea a través de un API o vistas de Django, queremos mostrar un listado de personas con su información de contacto, otro listado con la información de cuando se dieron de alta en el sistema o cuando actualizaron su información.

Como vimos en el post anterior, dado que los usuarios potenciales son todos los ciudadanos europeos, estamos hablando de millones de registros. Igual que reflexionamos sobre cómo conviene filtrarlos para devolver conjuntos más pequeños, en el volumen de datos también influye la cantidad de información que tiene cada registro.

Ante el problema que estamos abordando, podemos reflexionar y entender que no es lo mismo consultar registros Person leyendo sus diez atributos que haciéndolo solo de los tres o cuatro necesarios para la información que queremos mostrar.

Así que si nos encontramos con un caso como este, podemos aplicar técnicas que nos permitan reducir la información consultada a la base de datos, que también se verá reflejado en un menor uso de memoria.

Como solucionarlo

Tal como expusimos al plantear la resolución del problema de la cantidad de datos, podemos recordar que el volumen de datos es un factor importante a tener en cuenta al diseñar nuestras consultas.

Conviene que seamos conscientes de que el volumen de datos no solo se ve afectado por el número de registros que estemos consultando, sino que también influye el tamaño de los registros.

Es fácil comprender que tanto la base de datos como Django podrán responder más rápidamente y utilizando menos recursos si en vez de recuperar todas las columnas de un modelo al hacer una consulta, recuperamos solo las que necesitamos.

Como un gran poder conlleva una gran responsabilidad, hay que hilar muy fino y ser conscientes de la información que utilizaremos porque, si no lo hacemos bien, podríamos aumentar el número de consultas SQL como veremos más adelante.

El ORM de Django nos proporciona varios métodos para poder seleccionar las columnas que consultaremos en las tablas SQL com son el método values, el método values_list, el método only y el método defer.

El método values se utiliza para devolver un Queryset que contiene diccionarios con los campos seleccionados, en vez de instancias del modelo consultado. Este método acepta un parámetro fields que permite indicar qué campos se quieren obtener.

Utilizando este método, podemos obtener en un diccionario el nombre, apellidos y el pasaporte de todos los usuarios de un país:

Person.objects.filter(country_iso="ENG")

<Queryset [<Person: 1>, ….]>

Person.objects.filter(country_iso="ENG").values("id", "name", "surnames", "passport")

<Queryset [{"id": 1, "name": "PersonName", "surnames": "Person Surnames", "passport": "47855687P"}, ….]>

El método values_list es muy similar al método values. La principal diferencia la encontramos en que en lugar de devolver un Queryset de diccionarios devuelve un Queryset de tuplas.

En el caso de que solo se pase un campo al parámetro fields, se puede setear el parámetro flat a True para que devuelva un iterable global. Como curiosidad añadida, utilizando el parámetro named=True podemos hacer que las tuplas devueltas sean namedtuples.

A continuación planteamos el mismo ejemplo del caso anterior, pero utilizando values_list:

Person.objects.filter(country_iso="ENG").values_list("id", "name", "surnames", "passport")

<Queryset [(1, "PersonName", "Person Surnames", "47855687P"), ….]>

Person.objects.filter(country_iso="ENG").values_list("name", flat=True)

<Queryset ["PersonName", "PersonName2", "PersonName3"….]>

Person.objects.filter(country_iso="ENG").values_list("id", "name", "surnames", named=True)

<Queryset [Row(id=1, name="PersonName", surnames="Person Surnames",), ...]>

Como podemos ver en los ejemplos, si utilizamos estos métodos no tendremos acceso al resto de atributos del modelo al que hagamos la consulta. Es más, si intentamos acceder a una clave que no tiene el diccionario (caso values) o una posición que no tenga la tupla (caso values_list) se lanzará un error de Python. Por este motivo, al utilizar estos dos métodos hay que pensar muy bien qué campos consultamos a la base de datos.

Además de los métodos values y values_list, el ORM de Django nos proporciona dos opciones más que son los métodos only y defer. El uso de estos métodos es muy parecido a los dos anteriores y la gran diferencia entre los pares es que estos últimos devuelven objetos del modelo al que hemos hecho la consulta.

El método only devuelve un Queryset con objetos del modelo consultado donde solo se han recuperado los atributos indicados en el método, además del ID del objeto.

El método defer funciona de forma opuesta al método only, devuelve objetos del ORM que incluyen los campos que no se han indicado al método.

Como el ORM de Django solo recupera los campos que se les hayan indicado, si utilizamos un atributo que hayamos excluido del Queryset en vez de dar un error se ejecutará una nueva consulta SQL para recuperar ese valor para cada objeto. Esta es la segunda diferencia importante, que la información sí que es accesible en el ORM pero con una nueva consulta.

Como se indicaba más arriba, si no se utilizan bien estos métodos, en vez de reducir el rendimiento de las consultas lo incrementaremos, ya que estaremos haciendo un mayor número de consultas a la base de datos.

En los siguientes ejemplos podemos ver el uso de los métodos only y defer:

Person.objects.filter(country_iso="ENG").only("id", "name", "passport")

<Queryset [<Person: 1>, ….]>

Person.objects.filter(country_iso="ENG").defer("surnames")

<Queryset [<Person: 1>, ….]>

Traducción a SQL

Tal como se expuso en el post anterior, es importante tener en cuenta la conversión a SQL de las queries realizadas con el ORM de Django.

En el caso de los métodos que hemos visto en este artículo, la conversión es idéntica en cada caso, dado que la diferencia en el formato de los resultados obtenidos se encuentra en cómo los devuelve el ORM en cada método.

Utilizando estos métodos, lo que se puede modificar en las consultas SQL son los atributos a recuperar, indicados entre el SELECT y el FROM de las consultas. En el caso del método defer, el ORM actúa para que los campos en el ‘SELECT` sean los no indicados.

Person.objects.filter(country_iso="ENG").values("id", "name", "passport")
Person.objects.filter(country_iso="ENG").values_list("id", "name", "passport")
Person.objects.filter(country_iso="ENG").only("id", "name", "passport")
Person.objects.filter(country_iso="ENG").defer("created_at", "updated_at", "surnames", "phone", "email", "country")

SELECT id, name, passport WHERE country_iso = "ENG";

Conclusión

En este artículo hemos visto cómo resolver el problema de devolver más información de la necesaria de cada registro en nuestras consultas a la base de datos. Hemos expuesto que la solución al problema se encuentra en especificar qué campos queremos recuperar en nuestras consultas SQL, de forma que podemos reducir la cantidad de información, aligerando así el uso de recursos.

En la siguiente publicación hablaremos del problema "N + 1 Queries, JOINS", donde hablaremos sobre cómo reducir el número de consultas SQL cuando tenemos que realizar queries más complejas que necesiten consultar datos de más de una tabla.

Mencionar que la sección Queryset Api reference de la documentación oficial de Django está siendo utilizada como referencia en estos artículos.