July 20, 2016
This blog is part of our Rails 5 series.
Let's assume that in our health care application we have a page which shows all Patients. This page also has a filter and it allows us to filter patients by their name.
We could implement the filter as shown here.
Patient.where("lower(first_name) = ?", first_name.downcase)
There might be many users with the same name. In such cases, to speed up the
search process, we can add an index. But, adding a regular index will not
trigger an index scan since we are using an expression in the where clause i.e
lower(name)
. In such cases, we can leverage
expression indexes given by PostgreSQL.
Before Rails 5 adding an expression index is not straightforward since the
migrate api does not support it. In order to add one we would need to ditch
schema.rb
and start using structure.sql
. We would also need to add following
migration.
def up
execute <<-SQL
CREATE INDEX patient_lower_name_idx ON patients (lower(name));
SQL
end
def down
execute <<-SQL
DROP INDEX patient_lower_name_idx;
SQL
end
Rails 5 provides ability to add an expression index using add_index method as follows:
def change
add_index :patients,
'lower(last_name)',
name: "index_patients_on_name_unique",
unique: true
end
And we also get to keep schema.rb.
Time goes on. everyone is happy with the search functionality until one day a new requirement comes along which is, in short, to have partial matches on patient names.
We modify our search as follows:
User.where("lower(name) like ?", "%#{name.downcase}%")
Since the query is different from before, PostgreSQL query planner will not take the already existing btree index into account and will revert to a sequential scan.
Quoting directly from Postgresql documents,
'The operator classes text_pattern_ops, varchar_pattern_ops, and bpchar_pattern_ops support B-tree indexes on the types text, varchar, and char respectively. The difference from the default operator classes is that the values are compared strictly character by character rather than according to the locale-specific collation rules. This makes these operator classes suitable for use by queries involving pattern matching expressions (LIKE or POSIX regular expressions) when the database does not use the standard "C" locale.'
We need to add an operator class to the previous index for the query planner to utilize the index that we created earlier.
In order to add an index with an operator class we could write our migration as shown below.
def change
remove_index :patients, name: :index_patients_on_name_unique
add_index :patients, 'lower(last_name) varchar_pattern_ops',
name: "index_patients_on_name_unique",
unique: true
end
If this blog was helpful, check out our full blog archive.