¿Cuál es la diferencia entre select_related y prefetch_related en Django ORM?

291

En Django doc,

select_related() "sigue" las relaciones de clave externa, seleccionando datos adicionales de objetos relacionados cuando ejecuta su consulta.

prefetch_related() realiza una búsqueda por separado para cada relación y realiza la "unión" en Python.

¿Qué significa "hacer la unión en python"? ¿Alguien puede ilustrar con un ejemplo?

Entiendo que para una relación de clave extranjera, use select_related; y para la relación M2M, use prefetch_related. ¿Es esto correcto?

NeoWang
fuente
2
Realizar la unión en python significa que la unión no sucederá en la base de datos. Con select_related, su unión ocurre en la base de datos y solo sufre una consulta de base de datos. Con prefetch_related, ejecutará dos consultas y luego el ORM 'unirá' los resultados para que pueda escribir object.related_set
Mark Galloway
3
Como nota de pie de página, Timmy O'Mahony también puede explicar sus diferencias utilizando visitas a la base de datos: enlace
Mærcos
Esto puede ayudarlo a aprenderbatta.com/blog/working-with-select_related-in-django-89
anjaneyulubatta505

Respuestas:

424

Tu comprensión es mayormente correcta. Usas select_relatedcuando el objeto que vas a seleccionar es un solo objeto, por lo que OneToOneFieldo a ForeignKey. Usas prefetch_relatedcuando vas a obtener un "conjunto" de cosas, por lo que ManyToManyFields como dijiste o reviertes ForeignKeys. Solo para aclarar lo que quiero decir con " ForeignKeys inversa " aquí hay un ejemplo:

class ModelA(models.Model):
    pass

class ModelB(models.Model):
    a = ForeignKey(ModelA)

ModelB.objects.select_related('a').all() # Forward ForeignKey relationship
ModelA.objects.prefetch_related('modelb_set').all() # Reverse ForeignKey relationship

La diferencia es que se select_relatedune SQL y, por lo tanto, obtiene los resultados como parte de la tabla del servidor SQL. prefetch_relatedPor otro lado, ejecuta otra consulta y, por lo tanto, reduce las columnas redundantes en el objeto original ( ModelAen el ejemplo anterior). Puede usar prefetch_relatedpara cualquier cosa que pueda usar select_related.

Las compensaciones son que prefetch_relatedtiene que crear y enviar una lista de ID para seleccionar nuevamente al servidor, esto puede llevar un tiempo. No estoy seguro de si hay una buena manera de hacer esto en una transacción, pero entiendo que Django siempre envía una lista y dice SELECCIONAR ... DONDE pk IN (..., ..., ...) básicamente. En este caso, si los datos captados previamente son escasos (digamos objetos del Estado de EE. UU. Vinculados a las direcciones de las personas), esto puede ser muy bueno, sin embargo, si está más cerca de uno a uno, esto puede desperdiciar muchas comunicaciones. En caso de duda, pruebe ambos y vea cuál funciona mejor.

Todo lo discutido anteriormente es básicamente sobre las comunicaciones con la base de datos. Sin embargo, en el lado de Python prefetch_relatedtiene la ventaja adicional de que se utiliza un solo objeto para representar cada objeto en la base de datos. Con select_relatedobjetos duplicados se crearán en Python para cada objeto "padre". Dado que los objetos en Python tienen una carga de memoria decente, esto también puede ser una consideración.

CrazyCasta
fuente
3
¿Qué es más rápido?
Elad Silver
24
select_relatedes una consulta mientras prefetch_relatedque dos son, por lo que la primera es más rápida. Pero select_relatedno te ayudará por ManyToManyField's
bhinesley
31
@eladsilver Perdón por la lenta respuesta. De hecho depende. select_relatedutiliza un JOIN en el SQL mientras prefetch_relatedejecuta la consulta en el primer modelo, recopila todas las ID que necesita para buscar previamente y luego ejecuta una consulta con una cláusula IN en WHERE con todas las ID que necesita. Si ha dicho 3-5 modelos que usan la misma clave foránea, select_relatedseguramente será mejor. Si tiene cientos o miles de modelos que usan la misma clave externa, en prefetch_relatedrealidad podría ser mejor. En el medio tendrás que probar y ver qué pasa.
CrazyCasta
1
Discutiría su comentario sobre la captación previa "generalmente no tiene mucho sentido". Eso es cierto para los campos FK marcados como únicos, pero en cualquier lugar donde varias filas tengan el mismo valor FK (autor, usuario, categoría, ciudad, etc.), la captura previa reduce el ancho de banda entre Django y la base de datos, pero no duplica las filas. También generalmente usa menos memoria en la base de datos. Cualquiera de estos suele ser más importante que la sobrecarga de una sola consulta adicional. Dado que esta es la respuesta principal en una pregunta razonablemente popular, creo que debería tenerse en cuenta en la respuesta.
Gordon Wrigley
1
@GordonWrigley Sí, ha pasado un tiempo desde que escribí eso, así que volví y lo aclaré un poco. No estoy seguro de estar de acuerdo con el bit "usa menos memoria en el DB", pero sí a todo. Y seguramente puede usar menos memoria en el lado de Python.
CrazyCasta
26

Ambos métodos logran el mismo propósito, renunciar a consultas de db innecesarias. Pero usan diferentes enfoques para la eficiencia.

La única razón para usar cualquiera de estos métodos es cuando una única consulta grande es preferible a muchas consultas pequeñas. Django utiliza la consulta grande para crear modelos en la memoria de forma preventiva en lugar de realizar consultas bajo demanda en la base de datos.

select_relatedrealiza una unión con cada búsqueda, pero extiende la selección para incluir las columnas de todas las tablas unidas. Sin embargo, este enfoque tiene una advertencia.

Las combinaciones tienen el potencial de multiplicar el número de filas en una consulta. Cuando realiza una unión sobre una clave externa o un campo uno a uno, el número de filas no aumentará. Sin embargo, las uniones de muchos a muchos no tienen esta garantía. Entonces, Django se restringe select_relateda las relaciones que inesperadamente no resultarán en una unión masiva.

El "join in python" para prefetch_relatedes un poco más alarmante de lo que debería ser. Crea una consulta separada para cada tabla a unir. Filtra cada una de estas tablas con una cláusula WHERE IN, como:

SELECT "credential"."id",
       "credential"."uuid",
       "credential"."identity_id"
FROM   "credential"
WHERE  "credential"."identity_id" IN
    (84706, 48746, 871441, 84713, 76492, 84621, 51472);

En lugar de realizar una sola unión con potencialmente demasiadas filas, cada tabla se divide en una consulta separada.

cdosborn
fuente
1

Como dice la documentación de Django:

prefetch_related ()

Devuelve un QuerySet que recuperará automáticamente, en un solo lote, objetos relacionados para cada una de las búsquedas especificadas.

Esto tiene un propósito similar a select_related, ya que ambos están diseñados para detener el diluvio de consultas de la base de datos que se produce al acceder a objetos relacionados, pero la estrategia es bastante diferente.

select_related funciona creando una unión SQL e incluyendo los campos del objeto relacionado en la instrucción SELECT. Por este motivo, select_related obtiene los objetos relacionados en la misma consulta de la base de datos. Sin embargo, para evitar el conjunto de resultados mucho más amplio que resultaría de unirse a través de una relación 'muchos', select_related se limita a relaciones de un solo valor: clave externa y uno a uno.

prefetch_related, por otro lado, realiza una búsqueda por separado para cada relación, y realiza la 'unión' en Python. Esto le permite captar previamente muchos a muchos y muchos a uno objetos, lo que no se puede hacer usando select_related, además de la clave externa y las relaciones uno a uno que son compatibles con select_related. También admite la captación previa de GenericRelation y GenericForeignKey, sin embargo, debe restringirse a un conjunto homogéneo de resultados. Por ejemplo, los objetos de captación previa a los que hace referencia GenericForeignKey solo se admiten si la consulta está restringida a un ContentType.

Más información sobre esto: https://docs.djangoproject.com/en/2.2/ref/models/querysets/#prefetch-related

Amin.B
fuente
1

Pasó por las respuestas ya publicadas. Solo pensé que sería mejor si agrego una respuesta con un ejemplo real.

Digamos que tienes 3 modelos de Django que están relacionados.

class M1(models.Model):
    name = models.CharField(max_length=10)

class M2(models.Model):
    name = models.CharField(max_length=10)
    select_relation = models.ForeignKey(M1, on_delete=models.CASCADE)
    prefetch_relation = models.ManyToManyField(to='M3')

class M3(models.Model):
    name = models.CharField(max_length=10)

Aquí puede consultar el M2modelo y sus M1objetos relativos usando el select_relationcampo y los M3objetos usando el prefetch_relationcampo.

Sin embargo, como hemos mencionado M1, la relación de M2es a ForeignKey, solo devuelve 1 registro para cualquier M2objeto. Lo mismo se aplica OneToOneFieldtambién.

Pero M3la relación de M2es una ManyToManyFieldque podría devolver cualquier número de M1objetos.

Considere un caso en el que tiene 2 M2objetos m21, m22que tienen los mismos 5M3 objetos asociados con ID 1,2,3,4,5. Cuando busca M3objetos asociados para cada uno de esos M2objetos, si usa select related, así es como funcionará.

Pasos:

  1. Encuentra m21objeto.
  2. Consulta todos los M3objetos relacionados con el m21objeto cuyos ID son 1,2,3,4,5.
  3. Repita lo mismo para el m22objeto y todos los demás M2objetos.

Como tenemos mismas 1,2,3,4,5identificaciones tanto m21, m22los objetos, si utilizamos la opción select_related, que va a consultar la base de datos dos veces por los mismos identificadores que ya eran descabellada.

En cambio, si usa prefetch_related, cuando intente obtener M2objetos, tomará nota de todos los ID que devolvieron sus objetos (Nota: solo los ID) al consultar la M2tabla y, como último paso, Django hará una consulta a la M3tabla con el conjunto de todas las ID que sus M2objetos han devuelto. y unirlos a M2objetos usando Python en lugar de la base de datos.

De esta manera, está consultando todos los M3objetos solo una vez, lo que mejora el rendimiento.

Jarvis
fuente