Back to Insights

October 29, 2024

Boosting app performance with materialized views in Rails

Discover how materialized views in Rails can improve query performance for read-heavy apps. Learn setup and refresh techniques with SearchCraft!

rails

Introduction

As applications grow, performance bottlenecks can become a major concern, particularly when dealing with large datasets and complex queries. One powerful tool for improving query performance is the use of materialized views.
In this article, we’ll explore what materialized views are, how they differ from regular views, and how you can leverage them in a Ruby on Rails application using the SearchCraft gem. We’ll also look at a practical example where we aggregate sales data, focusing on how to set up, use, and refresh materialized views seamlessly.

What are materialized views?

A materialized view is a database object that stores the result of a query physically. Unlike a regular view, which acts like a virtual table that recalculates the query every time it’s accessed, a materialized view persists the data. This makes it significantly faster for read-heavy operations, particularly when dealing with complex queries or large datasets.
Materialized views are not exclusive to PostgreSQL; they’re also available in other relational databases like Oracle, MSSQL Server, and even some NoSQL database engines.

Materialized views versus regular views

Both materialized and regular views offer different advantages depending on your use case. Here’s a summary of their key differences.

Regular View Materialized View
Does not store data, only the query Stores the result of the query physically in the database
Recomputes the query each time it’s accessed Returns precomputed data for faster reads
Always up-to-date Can become stale, requires manual or scheduled refresh
Lightweight, for real-time data Ideal for heavy, complex queries that are run frequently

When to use materialized views

Materialized views are especially useful in the following scenarios:

  • Expensive queries: Queries with large joins, aggregations, or calculations are slow to execute. Materialized views cache the results, reducing the need for recomputation.
  • Read-heavy applications: When your app frequently reads data that doesn’t change often, materialized views reduce the load on the database.
  • Reporting & analytics: Dashboards or reports that involve heavy aggregations can benefit greatly from materialized views, allowing near-instant data retrieval.

In short, materialized views are perfect when you need fast read performance on complex or resource-intensive queries but can tolerate data that’s not real-time fresh.

Implementing materialized views in Rails with SearchCraft

Let’s walk through a practical example. We’ll optimize query performance in a Rails application by using SearchCraft to define a materialized view that aggregates sales information. The view will calculate total sales for each product and limit the results to the top 10 revenue-generating products.

Bottlenecks and issues we’re addressing

  • Expensive queries: Without a materialized view, calculating total sales involves a JOIN between products and orders tables with a SUM aggregation. As data grows, these queries become slow.
  • Frequent reads: In read-heavy applications (e.g., dashboards), running this query repeatedly puts a load on the database.
  • Performance optimization: Adding indexes to the materialized view (on id and total_sales columns) optimizes filtering and retrieval performance.

SearchCraft benefits

Instead of defining a materialized view directly in Postgres through migrations, we can use SearchCraft::Builder to simplify the process. This brings several advantages:

  • Cleaner code: No need for migrations; the view definition is tightly related to its associated, making it more maintainable.
  • Easier modifications: Updates to the view query and indexes can be made directly in the builder.
  • Easier indexation: The view_indexes method lets you define indexes on the materialized view at the same time.

Defining the materialized view

For our example, let’s assume that we have the following models backed by their corresponding tables

rails g model Product name:string
rails g model Client name:string
rails g model Order product:references client:references quantity:integer price:decimal

Here’s how to define the materialized view using SearchCraft. Create a new builder file at app/searchcraft/top_products_builder.rb:

class TopProductBuilder < SearchCraft::Builder
  def view_select_sql
    <<-SQL
      SELECT p.id, p.name, SUM(o.quantity * o.price) AS total_sales
      FROM products p
      JOIN orders o ON o.product_id = p.id
      GROUP BY p.id, p.name
      ORDER BY total_sales DESC
      LIMIT 10
    SQL
  end

  def view_indexes
    [
      { columns: ['id'], unique: true },
      { columns: ['total_sales'] }
    ]
  end
end

Interacting with the materialized view

To interact with the materialized view, we’ll create an ActiveRecord model that treats it like a regular database table. This allows us to perform queries and joins as we would with any other model.
Create the model for the materialized view at app/models/top_product.rb including SearchCraft::Model

class TopProduct < ApplicationRecord
  include SearchCraft::Model

  self.table_name = 'top_products'
  self.primary_key = :id

  belongs_to :product, foreign_key: :id
end

Also, let’s link the Product to TopProduct:

class Product < ApplicationRecord
  has_many :orders
  has_one :top_product, foreign_key: :id
end

If we run migrations now, even though there are no pending migrations, we’ll see the materialized view reflected in our database schema, created by Searchcraft using the builder we’ve defined:

--
-- Name: top_products; Type: MATERIALIZED VIEW; Schema: public; Owner: -
--

CREATE MATERIALIZED VIEW public.top_products AS
 SELECT p.id,
    p.name,
    SUM(((o.quantity)::numeric * o.price)) AS total_sales
   FROM (public.products p
     JOIN public.orders o ON ((o.product_id = p.id)))
  GROUP BY p.id, p.name
  ORDER BY (SUM(((o.quantity)::numeric * o.price))) DESC
 LIMIT 10
  WITH NO DATA;

Refreshing the materialized view

By default, materialized views are populated when they are created. However, if more data has been added to the tables after creation, you can refresh the view with:

TopProduct.refresh!

Or, to refresh all SearchCraft models in your app, use:

SearchCraft::Model.refresh_all!

You can refresh the materialized view either on a scheduled basis (e.g., through a background job) or whenever new data (like an order) is added, depending on your app’s requirements.

Querying the materialized view

Now that the materialized view is set up and up to date, we can start querying it.

To return the top-selling products, we can simply run:

TopProduct.all

Given that we have an index on the total_sales column, you can efficiently filter top-selling products based on a revenue threshold. For example, if you want to find all products that have generated at least $500,000 in total revenue, the query will utilize this index for faster performance:

TopProduct.where('total_sales >= ?', 500_000)

If you want to join top products with clients to find clients who ordered any of the top-selling products, you can use the following query as you would do with any ActiveRecord model. (don’t forget to add has_many :orders to the Client model)

Client.joins(orders: { product: :top_product }).distinct

This will return all clients who have purchased at least one of the top 10 revenue-generating products.

Maximizing Rails app performance with materialized views and SearchCraft for faster query results

Materialized views offer a simple yet powerful way to optimize performance for complex, read-heavy queries in your Rails app. By caching the results of expensive queries, you can drastically reduce query execution time, especially in reporting or analytics-heavy features.

Combining them with SearchCraft’s easy-to-use interface for defining views and indexes makes it even more seamless to integrate into a Rails application. Now you can start identifying those slow, frequent queries and leverage materialized views to supercharge your app’s performance.

Explore our Platform Engineering Studio

Our Platform Engineering Studio builds and optimizes the foundations for your business's success. From frontend and backend development to architecture design, we deliver tailored solutions.

Learn more
Ignacio Siri
Ignacio Siri

By Ignacio Siri

Full Stack Developer at Qubika

Ignacio Siri is a Full Stack Developer at Qubika, specializing in Ruby on Rails with over 10 years of experience. He likes to approach challenges with both empathy and pragmatism, striving to untangle complexity and embrace minimalism. Ignacio has led and contributed to various projects across diverse industries, including health, energy, and services.

News and things that inspire us

Receive regular updates about our latest work

Let’s work together

Get in touch with our experts to review your idea or product, and discuss options for the best approach

Get in touch