Criando índices concorrentes no Postgres com ActiveRecord

Publicado por Jean Matheus Souto no dia dev

A criação de índices no Postgres é simples e pode ser fundamental para o desempenho de sua aplicação. Isso não significa que devemos sair criando índices para tudo, pois se o índice não for utilizado acaba ocupando espaço e não sendo aproveitado para seu devido fim: performance.

Nosso time de produto respira métricas e performance. Isso faz com que fiquemos monitorando todas as nossas métricas de desempenho, e agir o mais rápido possível caso seja identificado algum ponto de melhoria.

Vamos olhar esse relatório do NewRelic de uma de nossas aplicações:

Relatório NewRelic sobre o Response Time

Ao interpretar o relatório percebemos que nosso response time estava lento, pois a consulta era muito utilizada e não tinha um índice para ela. Para resolver este problema apenas foi criado um índice simples, o que gerou uma diferença absurda no resultado final. Como podemos perceber, após o deploy do novo índice, o response time da consulta estabilizou e caiu drasticamente. :)

Otimizamos nossa performance e, o mais importante, deixamos nossos usuários mais felizes. :smile:

Let’s go, do it!

Quando uma aplicação cresce, a criação de índices fica um pouco mais complexa, dependendo o tamanho da tabela em que é criado. No entanto, isso não significa que não podemos criar índices. Temos que estudar as melhores opções para contornar esse problema e evitar downtimes na aplicação.

A criação de índice no Postgres faz um lock de escrita na tabela, por isso não é tão simples subir um índice para uma tabela que é muito utilizada. Temos que pensar que essa tabela estará sendo usada e terá apenas acesso para leitura.

Aqui entra a criação de índices de forma simultânea no Postgres. No Postgres temos a opção de criar um índice concorrente desta forma:

1
CREATE INDEX CONCURRENTLY index_users_on_email ON users(email)

Concorrência

Basta utilizar o CONCURRENTLY para o Postgres fazer o trabalho pesado:

CONCURRENTLY: When this option is used, PostgreSQL will build the index without taking any locks that prevent concurrent inserts, updates, or deletes on the table; whereas a standard index build locks out writes (but not reads) on the table until it’s done. There are several caveats to be aware of when using this option

Para ter o mesmo efeito utilizando ActiveRecord, no Rails 3 por exemplo, teríamos uma migração assim:

1
2
3
4
5
6
7
8
9
10
11
12
13
class AddLeadsManagerIndexToLeadImports < ActiveRecord::Migration
  def up
    execute "END"
    execute "CREATE INDEX CONCURRENTLY index_lead_imports_on_leads_manager_id ON lead_imports(leads_manager_id)"
    execute "BEGIN"
  end

  def down
    execute "END"
    execute "DROP INDEX CONCURRENTLY index_lead_imports_on_leads_manager_id"
    execute "BEGIN"
  end
end

Já usando o ActiveRecord 4, podemos criar uma nova migração para a criação do índice da seguinte maneira:

1
add_index :table, :column, algorithm: :concurrently

Ao utilizar o método add_index, podemos enviar o paramêtro algorithm descrevendo qual será a estratégia de criação do índice. Com isso estamos dizendo que o índice deve ser criado de forma concorrente.

Só isso? Não. Para que funcione, temos que entender como funciona a criação do índice. Vamos lá!

Por padrão, as migrações são executadas dentro de uma transação. Para a criação dos índices concorrentes, deve-se criar o índice fora da transação, esse comportamento é obtido chamando o método disable_ddl_transaction!:

1
2
3
4
# File activerecord/lib/active_record/migration.rb, line 425
def disable_ddl_transaction!
  @disable_ddl_transaction = true
end

Do it!

Vamos criar um índice de forma concorrente e com mais um plus: criaremos um índice parcial que será aplicado apenas nos registros que atenderem ao critério do seu where. Como tudo em Ruby deve ser, fica simples assim:

1
2
3
4
5
6
7
class AddIndexToConcurrentWorkflowByType < ActiveRecord::Migration
  disable_ddl_transaction!

  def change
    add_index :lead_nurturing_workflows, [:type], where: "type = 'ConcurrentWorkflow'", algorithm: :concurrently
  end
end

Data-driven

A utilização de índices é parte fundamental para melhorar a performance de uma aplicação. Sempre deve-se medir para ter claro qual o resultado que se quer atingir na adição do índice. Para finalizar, seja sempre data-driven. Dessa forma ficará muito mais fácil levar sua aplicação para o next-level*, otimizando cada vez mais sua performance.

Jean Matheus Souto

Jean Matheus Souto

Full Stack Developer

Comentários