livingintheshell : Django Performance - Tips and tricks

Performance vs Escalabilidade

Quando falamos em problemas de escalabilidade, normalmente se trata de um problema arquitetural. Um sistema não escalável horizontalmente é um sistema que não pode ser separado em diversas máquinas, exemplo simples:

Os arquivos de media estão no mesmo servidor de aplicação, que faz que não seja possível adicionar novas máquinas mantendo a semântica da aplicação.

Já em caso de performance, trata-se de algo mais pontual. Que pode deixar a resposta lenta, prejudicando a experiência do usuário. Que é o foco desse artigo.

Profile First

"Profile First", Django Docs

Não aplique soluções encontradas nesse artigo aleatoriamente, faça profilling, entenda o problema. Tentar corrigir sem entender ou conseguir reproduzir o problema, pode piorar a situação.

Leia antes esse artigo sobre ferramentas de profilling com Django.

Entenda lazy evaluation das consultas

Django docs

As consultas feitas via ORM (objetos QuerySet) só são realmente executadas quando certos métodos são aplicados, ex: iteração, len, list.

Então, coisas do tipo: len(Article.objects.all()) são bem perigosas, visto que todos objectos vão ser carregados em memória, pra que então seja feita a contagem.

Ao invés disso, deve-se preferir utilizar o método count para contagens. Se quisermos apenas saber se a consulta retorna algum item, o método exists é o ideal.

Exemplo:

# 1 consulta, e bem eficiente, visto que só traz a contagem
articles = Article.objects.all()
if articles.count() > 10:
    print(u'Alguma ação')

A exceção acontece se vamos percorrer todos objetos de uma queryset de qualquer forma, nesse caso, usar o count ou exists, vai na verdade, aumentar o número de consultas.

Exemplo:

# Ainda 1 consulta, se fosse usado o count aqui, seria uma consulta a mais
articles = Article.objects.all()[:10]
for article in articles:
    print article.title
print(len(articles))  # Ok, visto que os objetos já estão em cache

Outras funções que fazem com que a queryset seja avaliada:

  • Iteração
  • Pickle
  • bool
  • list
  • len
  • repr

Utilizando sub consultas com Django

Uma dica simples, mas muito legal é que o Django vai gerar uma subconsulta, caso seja passado uma outra queryset como argumento de um filtro por id.

Exemplo:

Ao passar uma lista de ids fazendo casting para lista, estamos executando a consulta e trazendo todos esses dados para a memória, pra depois passar novamente para o banco na segunda consulta.

Python:

# Estamos fazendo duas conultas, utilizando muita memória e rede.
Article.objects.filter(user__pk__in=list(User.objects.values_list('pk', flat=1)))

SQL gerado:

SELECT "teste_article"."id",
       "teste_article"."title",
       "teste_article"."body",
       "teste_article"."user_id"
FROM "teste_article"
WHERE "teste_article"."user_id" IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21)

Se passarmos a segunda queryset sem fazer casting para lista, o Django vai gerar uma única query com uma subconsulta!

Python:

# 1 consulta e uso mais eficiente de memória e rede
Model.objects.filter(user__pk__in=User.objects.values('pk')[:10])

SQL gerado:

SELECT "teste_article"."id",
       "teste_article"."title",
       "teste_article"."body",
       "teste_article"."user_id"
FROM "teste_article"
WHERE "teste_article"."user_id" IN
    (SELECT U0."id"
     FROM "teste_user" U0 LIMIT 10)

Use índices

Django docs

É essencial utilizar índices em campos que são constantemente utilizados para filtragem e ordenação. Isso pode otimizar muito certas consultas.

Mas cuidado, índices inúteis vão apenas piorar a performance do banco.

Exemplo:

class Article(models.Model):
    title = models.CharField(max_length=100, db_index=True)

Outras dicas

Utilize os métodos values ou values_list, na queryset, quando a instância do model não é necessária/interessante, ele irá retornar apenas os campos indicados como parâmetros, de forma bem eficiente.

Utilize os métodos only e defer, somente quando realmente necessário, tais métodos restringem campos que o ORM irá trazer do banco.

Utilize os métodos anotate e aggregate para agregação.

Utilize os métodos extra ou raw para consultas em sql puro, somente quando realmente necessário (ou seja, quando o ORM django não suporta o tipo de consulta).

O método assertNumQueries do TestCase do Django pode ser bem útil pra acompanhar o número de queries usadas em cada view.

Conclusão:

Queria nesse artigo mostrar formas de otimização pontuais, ficou fora do artigo as funções de cache do Django, assim como a utilização de outras fontes de armazenamento de dados, como o Redis.

Para posterior leitura, recomendo a Documentação do Django sobre o assunto: https://docs.djangoproject.com/en/dev/topics/performance/

Fernando Rocha is a passionate developer with interests in Python, Web development and Infrastructure. You can find him on twitter.
comments powered by Disqus