Diagnosing performance issues in Rails applications
By: Becca Liss
Tags: Code, Rails, Performance, Web Development, Ruby

Optimizing SQL queries

Sometimes, even if an ActiveRecord query does not produce any N + 1 queries, the SQL might still take a long time to execute. This is usually due to two things: missing indices, or a poorly-chosen SQL scan strategy.

How do I know if I have this problem?

Missing indices

If your query is performing any selection (WHERE / HAVING) or ordering, you generally want to have an index on those fields. For example, at VTS we have a table that shows users a list of their commercial real estate deals. Deals also have a status: they might be in an initial inquiry phase, they might be touring and showing available spaces to customers, or they might be in the final legal negotiations to close that deal. A user might want to view only deals in a specific stage, which could result in a query like this:

>>> Deal.where(status: [:currently_touring, :legal]) SELECT deals.* FROM deals WHERE deals.status IN (‘currently_touring’, ‘legal’)

Because this query performs a WHERE on a specific field (in this case a deal’s status), we want to make sure that we have an index on deals and statuses. If we added an order subquery to sort the deals alphabetically by name, we should add an additional index on the deal’s name. Another way to think about this is that we want to give the database a head-start for any queries we plan to run frequently.

(It’s worth noting that Rails automatically adds indexes for primary keys (usually id), and Rails 5 automatically adds an index for foreign keys if you use the references keyword in a migration)

Inefficient query plans

Databases have to make a best guess about the most efficient way to query tables. Although they are usually pretty good at this, they sometimes come up with inefficient plans.

A good way to see whether this might be the case is to run an EXPLAIN ANALYZE statement in your database console (note: not the rails console) and then use a tool like depesz to help understand the plan². Don’t worry if this looks like a lot of gibberish: there are a couple of main highlights you want to keep an eye out for:

Red and orange colors and/or high numbers lower down in the plan

The top of the query is always going to look scary because that’s the total time that the query takes to execute. You want to keep an eye out for moderately expensive queries that happen within a loop, because those may be getting executed thousands of times.

Looks scary – not that much

Although these numbers look scary, they are at the top of the query plan and inclusive which means that they include every operation that happens within that outermost Nested Loop. There’s no real point in trying to optimize this.

Check out the number of rows

This shows that the Nested Loop on row 14 is being performed over 52k rows, which is a lot. We might see whether we can perform some kind of filter operation earlier in the query, so that we can iterate over fewer rows.

High numbers in the exclusive column should be a red flag

This is notable because of the “exclusive” column, which means that this one Unique query took nearly 43 seconds to complete. This is probably a good candidate for optimization.

Seq(uential) scans

A sequential scan is when the database must go through every single row of a table to find a matching condition; although this might be efficient for tables with a very small number of rows, this can be extremely expensive on larger tables.

If you see that the database is doing a sequential scan instead of an index scan, this might be an indication that you are missing an index.

How do I fix it?

Rails Guides talks about adding indexes in their guide to ActiveRecord migrations. There are also gems that claim to identify missing indexes, though I haven’t tried them and can’t recommend any.

You can also use the query plan to help you identify performance “hot spots” and either make sub-queries more efficient or filter earlier in the query to reduce the number of rows that must be scanned.