Skip to content
DWKD

Template

Warehouse or Postgres Sizing Worksheet

Row count is the wrong argument. Three signals justify a warehouse - contention, sources, unpredictable concurrency - and a read replica settles most of them.

The question arrives as “have we outgrown Postgres.” It is almost never a question about size. Postgres holds more than the people asking believe, and teams who move for size alone find that whatever was actually slowing them down came along.

This worksheet makes you write down the signal before you write down a vendor. Most of the work is four queries against your own database, read honestly.

Why this exists

The failure mode is a migration argued from row count. It is the one number everybody in the room can produce, so it becomes the argument by default. It is also close to meaningless alone: a billion narrow rows scanned on a partition key is a different problem from fifty million wide rows joined six ways by eleven people at once.

Three signals actually justify the move, and you need at least one of them by name:

  • Contention. Analytical scans compete with the application’s own reads and writes on one instance. The symptom is that checkout slows down when somebody opens a report.
  • Sources. The questions require joining data that does not live in Postgres and never will — an ad platform, a billing processor, a spreadsheet somebody maintains. You are buying ingestion, scheduling and a transformation layer; the query engine is incidental.
  • Unpredictable concurrency. Many readers, each writing their own unbounded query, at hours you do not control. This is where separating storage from compute pays: you stop sizing one box for the worst thing anybody might type.

If you cannot name which of the three you have, you have a tuning problem inside Postgres, and it will follow you across the migration — harder to see, and costing more per hour to look at.

The step that gets skipped is the read replica. Pointing reporting traffic at a replica takes the analytical load off the primary at a fraction of the cost of a migration. A great many “we need a warehouse” symptoms are really “our dashboard is competing with the application.” Try it, measure again, and if the case still holds it is a stronger one.

How to use it

Fill in the measurements before anybody names a product. A vendor named on page one turns every measurement after it into supporting evidence.

Measure at the worst hour, not on average. Contention is bursty by definition, so an average hides precisely the thing you are trying to detect.

Write the counterfactual: what becomes possible that is not today, and who notices. “The data work gets less painful” is a legitimate answer — it is only a problem when something more impressive gets offered in its place.

Ask how the candidate bills — per compute-second, per byte scanned, or a flat instance — and price the most expensive plausible mistake on that model. An unfiltered scan inside a loop is a slow morning on a fixed instance and an invoice on a usage meter. Neither disqualifies anything; not knowing which you signed disqualifies you from the budget conversation.

And cost the migration, not the query engine. The engine is the cheap part. Ingestion per source, the transformation layer, a scheduler, access control, repointing every existing report — that is the project.

Where it goes wrong

The worksheet assumes the choice is open. If a warehouse already exists in the organization and people know how to run it, a second analytics home costs far more than any technical comparison shows, because two homes means two definitions of revenue. That cost is not on this sheet, and it is larger than everything that is.

It does not cover latency either. If somebody needs a number within seconds of the event that produced it, neither side of this comparison is the answer, and this sheet will quietly mislead you about that.

Do not use it to slow a decision already made for other reasons — a hiring plan, a platform standard set above you. Those are real reasons. Write them on the sheet as the reason and skip the measurements, rather than dressing them in numbers that fool nobody.

The measurements also expire. A worksheet from last year is a record of a decision, not evidence for the next one.

Copy and Clear become available when the worksheet is ready.

WAREHOUSE OR POSTGRES -- SIZING WORKSHEET

Fill top to bottom. Do not write a product name until section 4.

SYSTEM: FILLED BY: DATE:

DECISION OWNER: name, role -- the person who signs the invoice

  1. MEASURE -- against your own database, during your busiest hour

Instance RAM: GB vCPU: Storage used: GB

SHOW shared_buffers; ->

SHOW work_mem; ->

Largest tables:

SELECT relname, pg_size_pretty(pg_total_relation_size(relid))

FROM pg_catalog.pg_statio_user_tables

ORDER BY pg_total_relation_size(relid) DESC LIMIT 5;

Cache behavior on the tables reports touch:

SELECT relname, heap_blks_hit, heap_blks_read

FROM pg_statio_user_tables ORDER BY heap_blks_read DESC LIMIT 10;

Hit ratio on the worst offender: %

Low ratio on a big table = you read disk on every report run.

Worst queries (requires pg_stat_statements):

SELECT calls, mean_exec_time, total_exec_time, rows, query

FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 20;

Slowest single report: ms runs/day:

Largest total time consumer: ms/day

Share of total db time that is reporting: %

Contention -- sample every 30s through the busy hour:

SELECT state, wait_event_type, wait_event, count(*)

FROM pg_stat_activity GROUP BY 1,2,3 ORDER BY 4 DESC;

Peak concurrent active sessions:

Sessions waiting on locks or IO:

Does application latency move when a report runs? Y / N

Evidence, not opinion:

Vacuum keeping up:

SELECT relname, n_live_tup, n_dead_tup, last_autovacuum

FROM pg_stat_user_tables ORDER BY n_dead_tup DESC LIMIT 10;

Worst dead/live ratio: Last autovacuum there:

  1. THE THREE SIGNALS -- you need at least one, by name

Analytics and the application share one instance and fight.

Proof required: the latency evidence line above.

Systems you must join that are not in Postgres:

Name them:

If this is your signal, you are buying ingestion, scheduling

and a transformation layer. The query engine is incidental.

Distinct humans writing their own unbounded queries:

At hours you control? Y / N

Worst query anyone could plausibly type:

NONE CHECKED -> stay. What you have is a tuning problem, and it

migrates with you, where it is harder to see and costs more per hour.

  1. DO THIS FIRST -- the step that gets skipped

Read replica carrying all reporting traffic.

Already tried? Y / N

If N: stop here. Do it. Re-measure in two weeks.

If Y: what was still broken afterward:

Confirm these are exhausted, with dates:

  • Indexes checked against the actual report predicates
  • Rollup tables for the top three reports
  • Partitioning on the largest table's filter column
  • work_mem raised for the reporting role only

These are days of work. A migration is a quarter of it.

  1. COST THE MIGRATION, NOT THE QUERY ENGINE

Ingestion: sources x days = days

Transformation layer rebuilt = days

Scheduler and alerting = days

Access control and row-level rules = days

Repointing existing reports and exports = days

Dual-running both systems for weeks = days

Who does this work:

What stops while they do it:

BILLING MODEL OF THE CANDIDATE -- write it down before you sign

Most expensive plausible mistake on that model:

scenario:

what it costs: $

what prevents it:

  1. THE COUNTERFACTUAL -- one sentence, no hedging

After this, can that they cannot do today.

If the honest answer is "the data work gets less painful", write

that down. It is a real reason. It is only fatal when unstated.

WHAT THIS SHEET DOES NOT MEASURE

  • Second-home cost. If a warehouse already exists elsewhere, the

real price is two definitions of revenue, not two bills.

  • Sub-second latency needs. That is a streaming question and this

sheet will mislead you about it.

  • Anything six months out. Re-measure; do not re-read.