Otimizar consultas ao banco de dados é uma parte importante do desenvolvimento de aplicações web, e isso é especialmente verdadeiro quando se trata de aplicativos Ruby on Rails. Consultas lentas ou desnecessárias podem afetar significativamente o desempenho de um aplicativo, portanto, é importante identificar e corrigir essas consultas para garantir que o aplicativo esteja funcionando de maneira otimizada. Irei mostrar algumas dicas e técnicas de como identificar, medir, e decidir como otimizar suas consultas.

E não, não é só adicionar um index na coluna “X” da tabela de usuários.

Conjunção entre as Pleiades e o planeta Marte Conjunção entre as Pleiasdes e Marte, A captura foi no quintal da minha casa. A foto é o resultado de um empilhamento de mais de 50 fotos, com exposição de 2 segundos cada. A foto foi feita com uma câmera Canon T3i e uma lente de 85mm, ISO 1600, f/4.5.

Identificando as dores

Só se melhora o que se mede

Você não pode melhorar o que não consegue medir. Para identificar consultas lentas, você precisa medir o tempo de execução dessas consultas. Existem várias maneiras de fazer isso, desde ferramentas que coletam essas métricas onde sua aplicação está rodando até maneiras mais simples e devem ser analisadas antes mesmo que você envie seu código para produção.

Eu trato sobre alguns pontos em uma talk que dei no Ada.rb em 2021, você pode assistir a talk aqui.

EXAPLAIN

O EXPLAIN é uma ferramenta muito útil que serve para para analisar o plano de execução de uma consulta. O explain te mostra como a consulta é processada e quais índices são usados, entre outras informações úteis. Irei mostrar como este recurso pode te ajudar.

Os exemplos foram testados usando o Mysql.

1.1 Você pode fazer a análise direta no banco de dados:

EXPLAIN SELECT * FROM customers WHERE customer_id = 123;

ou se você estiver usando o ActiveRecord ❤️:

Customer.
  .where(customer_id: 123)
  .explain

A saída mostrará o plano de execução da consulta, incluindo:

  • quais tabelas são acessadas
  • quais índices são usados
  • e como a consulta é processada.

1.2 O EXPLAIN também pode ser usado para analisar consultas que envolvem JOIN:

EXPLAIN SELECT * FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
WHERE o.order_date > '2022-01-01';

ou se você estiver usando o ActiveRecord:

Order.
  .joins(:customer)
  .where('orders.order_date > ?', '2022-01-01')
  .explain

Vale lembrar, que o output irá depender da estrutura da tabela e dos índices que você criou. Um exemplo hipotético para uma tabela de clientes e uma tabela de pedidos:

EXPLAIN SELECT * FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
WHERE o.order_date > '2022-01-01';
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type  | possible_keys | key     | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | o     | NULL       | ALL   | NULL          | NULL    | NULL    | NULL |  100 |   100.00 | Using where |
|  1 | SIMPLE      | c     | NULL       | eq_ref| PRIMARY       | PRIMARY | 4       | o.c  |    1 |   100.00 | NULL        |
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+-------------+

Bullet

O Bullet é uma gem que ajuda a identificar e otimizar consultas lentas em sua aplicação. Ele monitora as consultas executadas em seu banco de dados e notifica você de possíveis problemas de desempenho, como consultas N+1 ou uso ineficiente de índices.

Para usar o Bullet, basta adicioná-lo à sua lista de dependências do Gemfile e instalá-lo executando o comando “bundle install”. Em seguida, basta habilitar o Bullet em seu arquivo de configuração config/environments/development.rb:

config.after_initialize do
  Bullet.enable = true
  Bullet.alert = true
  Bullet.bullet_logger = true
  Bullet.console = true
end

Isso habilitará o Bullet e configurará as notificações para serem exibidas no console, no log e como alertas em tempo real. Você também pode configurar o Bullet para enviar notificações por e-mail ou por outras mensagens.

Cuidado com consultas N+1

O N+1 pode ocorrer quando você executar uma consulta e, em seguida, excuta uma seguda consulta para cada linha retornada da primeira primeira consulta.

Quando o Bullet identificar um problema de desempenho, ele exibirá uma mensagem de alerta indicando o problema e sugerindo soluções possíveis. Isso é muito útil dirante o desenvolvimento, pois pode evitar que você insira as famigeradas consultas N+1 em seu código.

Um exemplo de alerta do Bullet:


# Bullet alert
# user: user
# GET /users
# USE eager loading detected
#   User => [:posts]
#   Add to your query: .includes([:posts])
# Call stack
#   /app/controllers/users_controller.rb:3:in `index'

Aqui, o Bullet detectou que você está executando uma consulta para obter uma lista de usuários e, em seguida, executando uma consulta para obter os posts de cada usuário, isso resulta em uma consulta N+1. O Bullet sugere que você adicione a opção includes para corrigir o problema.

outro exemplo:

Detected N+1 Query: Add to your finder: :includes => [:comments]
User Load (1.2ms)  SELECT "users".* FROM "users"
Comment Load (1.3ms)  SELECT "comments".* FROM "comments" WHERE "comments"."user_id" IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

Neste exemplo, ele detectou que a consulta está carregando os usuários e, em seguida, fazendo uma consulta separada para carregar os comentários de cada um deles, fazendo uma consulta adicional para cada linha retornada. A correção sugerida é parecida com a anterior, adicionar um includes para carregar os comentários de uma só vez, em vez de fazer uma consulta separada para cada usuário.

@users = User.includes(:comments).all

Confuso? Vamos ver um exemplo mais próximo da realidade:

Se você executar uma consulta para obter uma lista de posts e, em seguida, executar uma consulta para obter os autores(dados relacionados) de cada post, você poderá estar executando uma consulta N+1.

Ex.:

posts = Post.all
posts.each do |post|
  post.author
end

Isso pode ser resolvido adicionando um includes para carregar os dados relacionados de uma só vez, em vez de executar uma consulta para cada linha retornada.

Ex.:

posts = Post.includes(:author).all
posts.each do |post|
  post.author
end

Tem um post muito bom sobre isso no blog da bigbinary.

Consultas lentas, vai um índice aí?

Depois de identificar as consultas lentas, é importante analisá-las para entender por que elas estão lentas. Isso pode incluir examinar o esquema do banco de dados, o índice das tabelas e a estrutura das consultas em si. É possível que seja necessário adicionar índices ou alterar a estrutura das consultas para melhorar o desempenho, mas isso depende do problema em questão.

Consultas que usam ORDER BY em colunas que não são índices podem ser lentas. Isso porque o banco de dados precisa ler todas as linhas da tabela e, em seguida, classificá-las na memória antes de retornar os resultados.

No entanto, é importante lembrar que criar índices também pode afetar negativamente o desempenho das operações de inserção, atualização e exclusão, pois esses índices precisam ser atualizados sempre que os dados da tabela são alterados.

Também é importante consultar a documentação do banco de dados para obter mais informações sobre como criar índices e como eles afetam o desempenho. Se estiver usando Ruby on Rails, consulte a documentação do ActiveRecord para obter mais informações sobre como criar índices.

Testar e medir

Depois de otimizar as consultas lentas, é importante testar e medir o desempenho para garantir que as mudanças tenham o efeito desejado. Isso pode ser feito usando ferramentas de monitoramento e análise de performance, como o New Relic ou o DataDog, para comparar os resultados antes e depois da otimização.

Cache

Vou deixar este ponto para um próximo blog post. 😉

Conclusão

Agora você sabe como identificar consultas lentas e como otimizá-las. Isso pode ajudar a melhorar o desempenho do seu aplicativo e, em última análise, melhorar a experiência do usuário.

Referências