Qual é a diferença entre select_related e prefetch_related no Django ORM?

291

No Django doc,

select_related() "segue" relacionamentos de chave estrangeira, selecionando dados adicionais de objetos relacionados ao executar sua consulta.

prefetch_related() faz uma pesquisa separada para cada relacionamento e faz a "junção" no Python.

O que significa "fazer a junção em python"? Alguém pode ilustrar com um exemplo?

Meu entendimento é que, para relacionamento com chave estrangeira, use select_related; e para relacionamento M2M, use prefetch_related. Isso está correto?

NeoWang
fonte
2
Executar a junção em python significa que a junção não acontecerá no banco de dados. Com um select_related, sua associação acontece no banco de dados e você sofre apenas uma consulta ao banco de dados. Com prefetch_related, você será executar duas consultas e, em seguida, os resultados serão 'juntou' pelo ORM assim você ainda pode digitar object.related_set
Mark Galloway
3
Como nota de rodapé, Timmy O'Mahony também pode explicar as diferenças usando batidas de banco de dados: ligação
Mærcos
Isso pode ajudá-lo a learnbatta.com/blog/working-with-select_related-in-django-89
anjaneyulubatta505

Respostas:

424

Sua compreensão está correta. Você usa select_relatedquando o objeto que você selecionará é um único objeto, OneToOneFieldum ou mais ForeignKey. Você usa prefetch_relatedquando deseja obter um "conjunto" de coisas, ManyToManyFieldcomo você afirmou ou inverte ForeignKey. Apenas para esclarecer o que quero dizer com "reverse ForeignKeys", aqui está um exemplo:

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

A diferença é que select_relateduma junção SQL e, portanto, recupera os resultados como parte da tabela do servidor SQL. prefetch_relatedpor outro lado, executa outra consulta e, portanto, reduz as colunas redundantes no objeto original ( ModelAno exemplo acima). Você pode usar prefetch_relatedpara qualquer coisa que possa usar select_related.

As desvantagens são que prefetch_relatedé necessário criar e enviar uma lista de IDs para selecionar de volta ao servidor, isso pode levar um tempo. Não tenho certeza se existe uma boa maneira de fazer isso em uma transação, mas meu entendimento é que o Django sempre envia uma lista e diz SELECT ... WHERE pk IN (..., ..., ...) basicamente. Nesse caso, se os dados pré-buscados forem escassos (digamos, objetos do Estado dos EUA vinculados aos endereços das pessoas), isso pode ser muito bom; no entanto, se estiver mais próximo de um para um, isso pode desperdiçar muitas comunicações. Em caso de dúvida, tente os dois e veja qual funciona melhor.

Tudo discutido acima é basicamente sobre as comunicações com o banco de dados. No lado do Python, no entanto, prefetch_relatedhá o benefício extra de um único objeto ser usado para representar cada objeto no banco de dados. Com select_relatedobjetos duplicados serão criados em Python para cada objeto "pai". Como os objetos em Python têm um pouco de sobrecarga de memória, isso também pode ser uma consideração.

CrazyCasta
fonte
3
o que é mais rápido?
Elad silver
24
select_relatedé uma consulta enquanto prefetch_relatedsão duas; portanto, a primeira é mais rápida. Mas select_relatednão vai ajudá-lo para ManyToManyField's
bhinesley
31
@eladsilver Desculpe pela resposta lenta. Na verdade depende. select_relatedusa um JOIN no SQL, enquanto prefetch_relatedexecuta a consulta no primeiro modelo, coleta todos os IDs necessários para pré-buscar e, em seguida, executa uma consulta com uma cláusula IN no WHERE com todos os IDs necessários. Se você diz 3-5 modelos usando a mesma chave estrangeira, select_relatedcertamente será melhor. Se você possui centenas ou milhares de modelos usando a mesma chave estrangeira, prefetch_relatedpode ser realmente melhor. Entre você terá que testar e ver o que acontece.
22617 CrazyCasta
1
Eu contestaria seu comentário sobre a pré-busca relacionada "geralmente não faz muito sentido". Isso é verdade para os campos FK marcados como exclusivos, mas em qualquer lugar em que várias linhas tenham o mesmo valor de FK (autor, usuário, categoria, cidade etc etc), a pré-busca reduz a largura de banda entre o Django e o DB, mas não as linhas duplicadas. Também geralmente usa menos memória no banco de dados. Geralmente, um desses itens é mais importante que a sobrecarga de uma única consulta extra. Dado que esta é a resposta principal de uma pergunta razoavelmente popular, acho que isso deve ser observado na resposta.
Gordon Wrigley
1
@GordonWrigley Sim, já faz um tempo desde que escrevi isso, então voltei e esclarei um pouco. Não sei se concordo com o bit "usa menos memória no banco de dados", mas sim para tudo. E com certeza pode usar menos memória no lado do Python.
CrazyCasta
26

Ambos os métodos atingem o mesmo objetivo, para renunciar a consultas de banco de dados desnecessárias. Mas eles usam abordagens diferentes para eficiência.

O único motivo para usar um desses métodos é quando uma única consulta grande é preferível a muitas consultas pequenas. O Django usa a consulta grande para criar modelos na memória de preferência, em vez de executar consultas sob demanda no banco de dados.

select_relatedrealiza uma junção com cada pesquisa, mas estende a seleção para incluir as colunas de todas as tabelas unidas. No entanto, essa abordagem tem uma ressalva.

As junções têm o potencial de multiplicar o número de linhas em uma consulta. Quando você executa uma junção em uma chave estrangeira ou em campo individual, o número de linhas não aumenta. No entanto, junções muitos para muitos não têm essa garantia. Então, o Django restringeselect_related relações que não resultarão inesperadamente em uma junção massiva.

O "join in python" para prefetch_relatedé um pouco mais alarmante do que deveria ser. Ele cria uma consulta separada para cada tabela a ser unida. Ele filtra cada uma dessas tabelas com uma 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);

Em vez de executar uma única junção com potencialmente muitas linhas, cada tabela é dividida em uma consulta separada.

cdosborn
fonte
1

Como a documentação do Django diz:

prefetch_related ()

Retorna um QuerySet que recuperará automaticamente, em um único lote, objetos relacionados para cada uma das pesquisas especificadas.

Isso tem um objetivo semelhante ao select_related, pois ambos são projetados para interromper o dilúvio de consultas ao banco de dados causadas pelo acesso a objetos relacionados, mas a estratégia é bem diferente.

O select_related funciona criando uma junção SQL e incluindo os campos do objeto relacionado na instrução SELECT. Por esse motivo, select_related obtém os objetos relacionados na mesma consulta ao banco de dados. No entanto, para evitar o conjunto de resultados muito maior que resultaria da união em um relacionamento 'muitos', o select_related é limitado a relacionamentos de valor único - chave estrangeira e um a um.

prefetch_related, por outro lado, faz uma pesquisa separada para cada relacionamento e faz a 'junção' no Python. Isso permite a busca prévia de objetos muitos para muitos e muitos para um, o que não pode ser feito usando o select_related, além da chave estrangeira e dos relacionamentos one-to-one que são suportados pelo select_related. Ele também suporta a pré-busca de GenericRelation e GenericForeignKey, no entanto, deve ser restrito a um conjunto homogêneo de resultados. Por exemplo, a pré-busca de objetos referenciados por uma GenericForeignKey é suportada apenas se a consulta estiver restrita a um ContentType.

Mais informações sobre isso: https://docs.djangoproject.com/en/2.2/ref/models/querysets/#prefetch-related

Amin.B
fonte
1

Passou pelas respostas já postadas. Apenas pensei que seria melhor se eu adicionar uma resposta com exemplo real.

Digamos que você tenha 3 modelos de Django 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)

Aqui você pode consultar o M2modelo e seus M1objetos relativos usando select_relationcampo e M3objetos usando prefetch_relationcampo.

No entanto, como mencionamos M1a relação de M2é a ForeignKey, ela retorna apenas 1 registro para qualquer M2objeto. O mesmo se aplica OneToOneFieldtambém.

Mas M3a relação de M2é uma ManyToManyFieldque pode retornar qualquer número de M1objetos.

Considere um caso em que você tem 2 M2objetos m21, m22que possuem os mesmos 5M3 objetos associados aos IDs 1,2,3,4,5. Quando você busca M3objetos associados para cada um desses M2objetos, se você usa o select related, é assim que funciona.

Passos:

  1. Encontre um m21objeto.
  2. Consulta todos os M3objetos relacionados ao m21objeto cujos IDs são 1,2,3,4,5.
  3. Repita a mesma coisa para o m22objeto e todos os outros M2objetos.

Como temos os mesmos 1,2,3,4,5IDs para ambos m21, m22objetos, se usarmos a opção select_related, ele consultará o banco de dados duas vezes pelos mesmos IDs que já foram buscados.

Em vez disso, se você usar prefetch_related, ao tentar obter M2objetos, ele anotará todos os IDs que seus objetos retornaram (Nota: apenas os IDs) enquanto consultava a M2tabela e, como último passo, o Django fará uma consulta à M3tabela com o conjunto de todos os IDs que seus M2objetos retornaram. e junte-os a M2objetos usando Python em vez de banco de dados.

Dessa forma, você está consultando todos os M3objetos apenas uma vez, o que melhora o desempenho.

Jarvis
fonte