Rails, PostgreSQL e o uso de índices
03/05/2014Update:
“Vale lembrar que campos com índices únicos tratam maiúsculas e minúsculas
como caracteres diferentes. Um campo username com índice único irá aceitar,
por exemplo, os valores john
, John
e JOHN
como valores únicos.
Para evitar que isso aconteça, use a extensão citext.”
Dica do Nando
Confira em: simplesideias.com.br
Ultimamente tenho feito alguns trabalhos que envolvem “melhorias” de software, ajustes para aumentar o desempenho em produção ou simplesmente para fazer funcionar da forma correta.
As melhorias abordam coisas simples como fazer uso correto do ActiveRecord, utilizar da melhor forma os recursos da tecnologia escolhida. Em boa parte dos casos o uso de índices e correções no uso do ActiveRecord já resolve bastante.
Para que serve um índice ?
Em banco de dados, um índice é uma referência utilizada para otimizações, isso permite que um registro seja localizado de forma mais rápida em uma consulta.
O PostegreSQL possui várias opções de índices, porém o tipo mais comumente utilizado é o B-tree.
Índice de chave primária
É comum adicionarmos um índice para as chaves primárias de nossas tabelas, mas no caso do PostgreSQL isso é feito de forma automática, por tanto não precisamos criar de forma explícita.
Quando geramos uma migration:
class CreateUser < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :name
t.string :email
t.timestamps
end
end
end
A saída no psql será a seguinte:
hfh_development=# \d users
Table "public.users"
id integer not null default nextval('users_id_seq'::regclass)
name character varying(255) not null
email character varying(255) not null
Indexes:
"users_pkey" PRIMARY KEY, btree (id)
Na linha 10 podemos ver que o PostgreSQL adicionou o índice B-tree na chave
primária, neste exemplo no campo id
.
Aplicações Rails com baixo desempenho por falta de índices
Imagine um fórum que possui muitas perguntas organizadas por categorias, sem o uso de índices a busca seria bastante lenta de ser realizada. A falta de índices em chaves estrangeiras é um dos problemas mais comuns que provocam o baixo desempenho em apps Rails.
O uso de índices em relacionamentos poderia ser dessa forma:
class CreateQuestions < ActiveRecord::Migration
def change
create_table :questions do |t|
t.string :title
t.text :content
t.belongs_to :category, index: true
t.timestamps
end
end
end
Ao adicionar index: true
na migration para o relacionamento, o PostgreSQL
gerou um índice chamado index_questions_on_category_id
que aponta para o
campo category_id
:
hfh_development=# \d questions
Table "public.questions"
id integer not null default nextval('questions_id_seq'::regclass)
title character varying(255) not null
content text not null
category_id integer not null
Indexes:
"questions_pkey" PRIMARY KEY, btree (id)
"index_questions_on_category_id" btree (category_id)
Isso faz toda diferença nas consultas feitas pela aplicação.
Evitando registros duplicados
Pois bem, em alguns dos trabalhos encontrei o problema de registros duplicados no banco de dados, isso normalmente ocorre por falta do uso de índices para impor unicidade no valor salvo (apenas validar no Rails não vai resolver o problema).
Imagine dois usuários tentando fazer um cadastro utilizando o mesmo e-mail e a
sua aplicação possuí apenas o validates_uniqueness_of
no model para validar a
unicidade do e-mail.
Ao submeterem o formulário preenchido ao mesmo tempo, o Rails vai verificar na tabela de usuários para saber se já existe algum registro com o e-mail fornecido , não encontrando nada ele responde que pode prosseguir e acaba permitindo o registro de 2 e-mails iguais mandando a validação de unicidade pro espaço.
Para evitar isso, podemos fazer uso de índices de unicidade:
class CreateUser < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :name
t.string :email
t.timestamps
end
add_index :users, :email, unique: true
end
end
Adicionando o unique: true
na migration o PostgreSQL irá gerar o seguinte
índice no banco:
hfh_development=# \d users
Table "public.users"
id integer not null default nextval('users_id_seq'::regclass)
name character varying(255) not null
email character varying(255) not null
Indexes:
"users_pkey" PRIMARY KEY, btree (id)
"index_users_on_email" UNIQUE, btree (email)
Agora é possível garantir a integridade dos dados e evitar a duplicação de registros.
Índices parciais
Basicamente um índice parcial é definido por uma expressão condicional, em
outras palavras ele faz uso da cláusula where
, dessa forma ele é construído
apenas com as informações que satisfazem a condição criada.
Imagine que no aplicativo de fórum temos uma tabela para perguntas e nessa tabela contém um campo para marcar perguntas como respondidas (true) ou não respondidas (false), podemos criar um índice parcial para filtrar as não respondidas e melhorar o desempenho na consulta para exibi-las:
class CreateQuestions < ActiveRecord::Migration
def change
create_table :questions do |t|
t.string :title
t.text :content
t.boolean :answered, default: false
t.timestamps
end
add_index :questions, :answered, where: "answered = false"
end
end
O PostegreSQL vai gerar o seguinte índice:
hfh_development=# \d questions
Table "public.questions"
id integer not null default nextval('questions_id_seq'::regclass)
title character varying(255) not null
content text not null
answered boolean default false
Indexes:
"questions_pkey" PRIMARY KEY, btree (id)
"index_questions_on_answered" btree (questions) WHERE answered = false
Espero que tenha ficado claro a importância do uso de índices. Happy Hacking ;)