← Back to Insights

Reporting architecture

Why Reporting Features Are So Expensive

Written by:Better SoftwareBetter Software TeamUpdated 9 min read
Why Reporting Features Are So Expensive

Quick Answer

Reporting features cost more than expected because analytical queries compete with transactional workloads on the same database. Running reports directly against the operational database slows the core product, complicates history and security, and drives up infrastructure cost. A better path separates reporting data early, captures business events as facts, builds small purpose-built data marts, and exposes results through a dedicated reporting API.

Reporting features look simple and rarely are. We see the same pattern in many products. You’ve built out the core workflow of your product. Users are creating records, moving them through stages, and getting work done. Now the natural next step is that they want to see what's happening. Maybe it's a simple count of completed tasks, a breakdown of item statuses, or a trend of activity over time. Management asks for a dashboard, sales wants to track conversions, and support needs to see common issues. You think, "It's just SQL; we can pull that directly from the database." That's where things get expensive, fast.

Direct database queries for reporting, while seemingly efficient for simple cases, quickly become a bottleneck and a structural liability. As reporting demands grow, with more complex filters, date ranges, aggregations, and cross-entity joins, the performance of your operational database degrades. Developers spend increasing amounts of time optimizing queries, applying indices, and battling deadlocks, all for features that don't directly advance the core product workflow.

Why Simple Queries Break Reporting Features

Your primary database is optimized for transactional workloads, such as quick inserts, updates, and reads of small, discrete data sets. It's built for consistency and immediate availability of the latest state. Reporting, on the other hand, is analytical. It demands aggregations over large datasets, historical snapshots, and complex joins across many tables. We find these two access patterns fundamentally at odds, leading to performance issues and architectural compromises.

Consider a system tracking customer orders. For operational use, you need to quickly retrieve a specific order's details or update its status. This is fast. For reporting, you might need to count all orders placed last month, group them by region, calculate average order value over the last quarter, or identify the top 10 selling products in each category. These queries touch a vast number of rows, perform costly aggregations, and can monopolize database resources, slowing down the very operational processes the database is supposed to support.

"The moment you treat your operational database as your analytical database, you've started digging an architectural debt hole you'll pay interest on for years. They serve different masters with different performance profiles."

What Breaks in Practice With a Naive Reporting Approach

We see six problems show up when reports run straight off the operational database.

  • Performance Bottlenecks. Complex reports lock tables, consume excessive CPU, and fill connection pools, making the core application sluggish or unresponsive. Users complain about slow page loads for basic operations.
  • Data Inconsistency & Complexity. What does 'active user' mean historically? If a user changes their name or status, should reports from last year reflect the old name or new? Operational schemas are often optimized for normalization, making historical reporting or complex aggregations verbose and slow to query.
  • Security & Access Control Bleed. Granting report access often means exposing direct database access or building complex, redundant logic to filter data according to user roles, which can be an unnecessary risk.
  • Feature Creep & Developer Burnout. Every new reporting request becomes a major development effort. Developers are constantly optimizing SQL, instead of building new product features, leading to frustration and slow product iteration.
  • Expensive Infrastructure. To cope with the load, you scale up your primary database, which is typically the most expensive component of your infrastructure, far beyond what the operational workload alone would require.
  • Difficulty with External Integrations. If stakeholders want reports incorporating data from other systems (e.g., Salesforce, accounting software), combining this with your operational data in a single transactional database becomes a nightmarish ETL problem.

How to Build Reporting Iteratively Without Massive Tech Debt

Our advice is to acknowledge early that analytical workloads require a different architectural pattern than transactional ones. Don't start with a full-blown data warehouse, but gradually introduce separation and specialized components. Caching helps with repeat reads, as we cover in why caching strategy matters more than most founders think.

1. Decouple Your Reporting Data From Your Operational Data Early

Initially, you might pull data from your primary database into a separate, read-only database (a 'replica') specifically for reporting. This immediately offloads some of the load. As reporting needs grow, consider a dedicated analytical data store. This could be a simpler, denormalized schema in a PostgreSQL instance or a specialized analytical database like ClickHouse or a column-store database. We compare the underlying data store choice in how to choose between SQL and NoSQL for your first product.

2. Define “Reportable Events” and Capture Them as Streams

Instead of relying on the current state in your relational tables, treat key business events (e.g., 'Order Placed', 'Item Shipped', 'User Registered') as immutable facts. Emit these facts into an event stream (e.g., Kafka, AWS Kinesis). This event stream becomes the source of truth for your reporting system, allowing you to reconstruct historical states or aggregate events without taxing your operational database. This enables powerful event-driven architectures.

3. Build Small, Purpose-Built Materializations (Data Marts)

For specific reports, create highly denormalized tables optimized for that report's queries. For example, if you need a 'daily sales by region' report, create a table that already has daily sales and region aggregated. Update these tables periodically (e.g., nightly, hourly) from your event stream or analytical store. This is faster than calculating on demand and avoids complex queries on live data.

4. Implement a Dedicated Reporting API or Service

Instead of allowing direct access to reporting data sources, expose reports through an API. This allows you to encapsulate query logic, apply security, cache results, and swap data sources without impacting front-end applications. It separates concerns and enforces architectural boundaries.

5. Start With Pre-Rendered Reports, Then Move to Interactive Dashboards

For your Version 1, don't aim for a fully interactive dashboard. Start with simple, pre-calculated reports that are emailed daily or viewable on a static page. As usage and demand grow, introduce more interactive elements, backed by your analytical store and optimized materialized views. This allows you to learn what reports are truly valuable before investing heavily in real-time interactivity. This aligns well with the iterative development philosophy we use in product engineering.

Practical Decision Framework for Reporting Architecture

When considering a new reporting feature, we ask these questions:

  1. What's the immediate user need? Is it a one-time data dump, a daily summary, or real-time monitoring? (Don't over-engineer for real-time if a daily summary suffices initially.)
  2. What's the data freshness requirement? Is it 'up to the minute', 'today's data', or 'yesterday's data'? (Lower freshness requirements allow for simpler, cheaper batch processing.)
  3. What's the data volume and query complexity? Are we querying thousands of rows or billions? Simple filters or complex joins and aggregations? (High volume/complexity points away from the operational database.)
  4. Does this report require historical data that changes over time? E.g., 'number of active subscriptions last year' where 'active' is defined slightly differently now. (If so, event sourcing or clear snapshots are critical.)
  5. Can this be satisfied with a read-only replica or a simple materialized view? (If yes, start here. It's the lowest-cost separation.)
  6. Are there external data sources that need to be combined? (If yes, a dedicated reporting layer with ETL capabilities becomes essential.)
  7. Who will use this report, and what's their technical comfort? (Ad-hoc reporting tools for technical users vs. fixed dashboards for business users.)

Build Reporting Features Without Accumulating Debt

Reporting features are deceptively complex. What looks like a simple SELECT statement can quickly expose deep architectural flaws and create significant technical debt. By proactively separating analytical concerns from operational ones, focusing on event-driven data capture, and building specialized data marts incrementally, you can deliver valuable reporting without crippling your core product or accumulating insurmountable costs. Building a robust reporting architecture isn't about throwing money at a data warehouse. It's about making deliberate, iterative technical choices that acknowledge the different demands of analytical workloads.

If you are building a product and want to avoid the expensive mistakes, start with Better Software, where we help founders scope and build version one without the rework.

Frequently Asked Questions

What Is the Difference Between an Operational Database and an Analytical Database?

We describe an operational database, such as PostgreSQL or MySQL, as one optimized for many small, frequent reads and writes of current application state. An analytical database, such as ClickHouse or a data warehouse, is optimized for fewer, larger queries that aggregate historical data across many rows and tables for reporting.

When Should I Move My Reporting Off My Main Transactional Database?

We recommend moving reporting off the main transactional database as soon as report generation slows the core application, or when reporting becomes complex enough to need significant optimization work on the transactional schema. Data freshness needs that differ between reports and operations are another signal.

What Are “Materialized Views” in the Context of Reporting?

We use a materialized view as a stored query result that is refreshed periodically instead of being computed on every access. For reporting, it pre-calculates complex aggregations or joins, so later queries run much faster, trading storage space and refresh latency for query speed.

Do I Need a Data Warehouse for Reporting?

We would not start with a full-blown data warehouse. Begin by separating reporting data from operational data, for example with a read-only replica, then add a dedicated analytical data store as reporting needs grow and the simpler setup stops keeping up.

How Do Data Marts Help With Reporting?

We build small, denormalized tables for a specific report, such as daily sales by region, and refresh them nightly or hourly from the event stream or analytical store. That is faster than calculating on demand and avoids complex queries on live data.

About the author

Better Software

Better Software Team

Product and engineering team

Better Software Team is the product and engineering team at Better Software. We build new software products and improve existing applications and systems, with deeper experience in healthcare, finance and energy.