Reporting rarely shows up as a problem on day one.
It usually starts with something small and useful. You set up a dashboard, a few queries, and maybe a quick way to answer a question that used to take too long.
This seems to work fine, so it gets left alone.
Then the app grows.
With more data, more questions, expanded filters, and more metrics, this process begins to carry more weight than it was designed for.
At some point, it stops feeling simple.
We start to see reporting become one of the slowest, most complex parts of an otherwise healthy Rails application.
The pattern we keep running into
What’s interesting is that this doesn’t happen because of one bad decision. It’s usually a series of reasonable ones.
A query gets a little more complex. Then another layer gets added. Active Record turns into raw SQL. Indexes are introduced to keep things fast. Views are added to organize the logic.
Each step makes sense on its own, right?
But over time, the system becomes harder to understand, slower to run, and more difficult to change. Upgrades get riskier, and even small changes start to feel expensive.
Teams often try to fix this by adding more indexes or introducing views, but they don’t make queries faster. Materialized views can work for smaller datasets, but become costly to refresh as things grow
None of these approaches are wrong. They just don’t always address the root of the problem.
A shift in how we approach reporting
Instead of continuing to optimize increasingly complex queries, we step back.
Most reporting is time-based: Days. Weeks. Months.
That gives us a clue.
Rather than calculating everything on the fly, we look for the smallest useful unit of data. Often that’s a day. Sometimes it’s something else. But there’s usually a clear base unit underneath the complexity.
Once you find that, you can start shaping the data in advance rather than rebuilding it on every request.
What tends to work better in practice
From here, a few patterns make reporting systems much easier to work with as they grow.
We create tables that are designed specifically for reporting, storing only the data needed to answer those questions. Instead of querying across many tables with complex joins, we aim to keep reporting queries focused and predictable.
We move heavy data processing out of request time, so user-facing performance doesn’t compete with reporting workloads. In some cases, this means using a follower database or a separate process to prepare reporting data in the background.
And we accept a small tradeoff: not everything needs to be real-time. In many cases, slightly delayed but fast and reliable reporting is a much better experience than slow, live queries.
We’re walking through this LIVE
We’ve been seeing this pattern often enough in client work that we decided to put together a short session to walk through how we approach it.
If reporting has started to feel heavier than it should, you’re welcome to join us.

Top comments (0)