TL;DR. Database cost optimization means pricing every piece of waste in the estate and cutting in dollar order. In a hypothetical estate of 12 Postgres instances on RDS and Cloud SQL ($3,729.45 a month at list prices), staging and dev running all week, a replica with no readers and one oversized primary hold $1,151.49 a month. The 180 GB of unused indexes and tables on the two RDS primaries, the waste everyone lists first, save $0.00, because RDS (like Cloud SQL) bills allocated storage, until a separate shrink brings the most it can, $51.75. What those indexes do cost is writes: in our local PostgreSQL 18.6 test, two unused indexes raised the WAL of a 1 million row insert by 77%, and one on an updated column cut HOT updates from 83.9% to 1.1%.

Database cost optimization starts with a ranked list, not a list of tips. Most of a managed database bill is compute, so the waste with the largest dollar figure is almost always an instance running with nothing to do. The objects inside each database come last in dollars, and on RDS and Cloud SQL dropping them does not change the bill on its own.

Prices below are list prices: the AWS RDS price list for US East (published November 13, 2026) and the Cloud SQL pricing page for Enterprise edition in Iowa (us-central1), both read November 14, 2026, over a 730-hour month. The estate is a model, not a measured bill. The index figures were measured on a local PostgreSQL 18.6 server, as described below.

Where a database estate's money goes: 80% compute, then allocated storage

Consider a team running Postgres on two clouds: eight RDS for PostgreSQL instances and four Cloud SQL for PostgreSQL instances, with production, replicas, staging and dev.

InstancesSpecEach, per monthTotal
2 RDS production primariesdb.r6g.xlarge Multi-AZ ($0.899 an hour), 500 GB gp3 ($0.23 per GB-month)$771.27$1,542.54
2 RDS read replicasdb.r6g.xlarge ($0.45 an hour), 500 GB gp3 ($0.115)$386.00$772.00
2 RDS stagingdb.r6g.large ($0.225 an hour), 500 GB gp3$221.75$443.50
2 RDS devdb.t4g.medium ($0.065 an hour), 100 GB gp3$58.95$117.90
1 Cloud SQL production4 vCPU, 16 GiB, 250 GB SSD, high availability$489.71$489.71
1 Cloud SQL staging2 vCPU, 8 GiB, 250 GB SSD$143.68$143.68
2 Cloud SQL dev1 vCPU, 3.75 GiB, 50 GB SSD$57.81$115.62
RDS backup storage2,800 GB automated plus 1,500 GB manual, 3,200 GB free1,100 GB at $0.095$104.50
Estate$3,729.45

Cloud SQL lines use $0.0413 per vCPU-hour, $0.007 per GiB-hour of memory and $0.17 per GiB-month of SSD, doubled for high availability; Google prices storage per GiB, and we read the sizes as GiB. RDS gives backup storage free up to 100% of total database storage in the Region, here 3,200 GB.

Compute is $2,997.45 of the $3,729.45 (80.4%), allocated storage $627.50 (16.8%) and backups $104.50 (2.8%). The word that matters is allocated. RDS's storage guide says you can raise a volume but "You can't deallocate space", and Cloud SQL lists storage capacity as "Increase only" among the settings you can edit. Freeing bytes inside the database leaves the storage line where it was.

Measure idle compute per instance before anything else

Idle compute comes in three shapes, and each has its own reading:

  1. Non-production running all week. Pull the hourly maximum of connections for two weeks (DatabaseConnections in CloudWatch, the connections metric in Cloud Monitoring). If staging and dev see clients only on weekdays from 08:00 to 18:00, they need 217.26 of 730 hours.
  2. Replicas with no readers. Read the replica itself: its connections and its own pg_stat_user_tables counters. A replica that only receives WAL is a standby you pay for at the same rates as a standard instance.
  3. Oversized primaries. CPU and memory percentiles over 28 days, read against the peak, not the average.

In the model, suppose the readings find all three. The seven staging and dev instances cost $437.72 a month more running all week than on a 50-hour schedule. RDS stops them for up to seven days at a time, as our stop RDS instance guide prices. Cloud SQL drops instance charges when the activation policy is NEVER, while "charges for storage and IP addresses continue to apply". On both, the schedule cuts only the hours. The second replica turns out to have no reader ($386.00). The second primary runs low enough to move from db.r6g.xlarge to db.r6g.large Multi-AZ ($0.899 to $0.45 an hour, $327.77 a month). Per-instance measurement on RDS, with the queries for connected but unused instances, is in our RDS cost optimization guide.

Unused indexes cost writes, not storage dollars

Find them on every server, primary and replicas, because PostgreSQL keeps cumulative statistics in each server's shared memory:

SELECT s.relname AS table_name,
       s.indexrelname AS index_name,
       s.idx_scan, s.last_idx_scan,
       pg_size_pretty(pg_relation_size(s.indexrelid)) AS index_size
FROM pg_stat_user_indexes s
JOIN pg_index i ON i.indexrelid = s.indexrelid
WHERE s.idx_scan = 0
  AND NOT i.indisunique AND NOT i.indisprimary
ORDER BY pg_relation_size(s.indexrelid) DESC;

The counters prove less than they seem. pg_stat_reset() zeroes them, and the docs say "all statistics counters are reset" when a server starts from an unclean shutdown, a base backup or point-in-time recovery. An index used only by a month-end report shows zero for 29 days. Read idx_scan = 0 over at least one full business cycle, and keep the readings outside the database so a reset shows.

To see what these indexes cost, we measured them. Setup: PostgreSQL 18.6 on an Intel Core i5-6200U (2 cores, 4 threads), default settings except synchronous_commit = off (it changes commit waits, not WAL volume), with full_page_writes on. Then:

  • Load a 7-column orders table with 1 million rows in one INSERT ... SELECT and measure WAL with pg_wal_lsn_diff.
  • Run 100,000 single-row updates that set status and updated_at on random rows, each committed on its own.
  • Repeat each scenario three times on a fresh table. WAL varied by under 1% between runs, and the medians are below.
ScenarioInsert WALInsert timeUpdate WALHOT updates
Primary key only198.6 MB6.4 s151.3 MB83.9%
Plus 2 unused indexes (customer_id, created_at)351.8 MB13.6 s182.3 MB83.9%
Plus an unused index on status416.7 MB15.9 s206.8 MB1.1%

Two indexes nobody read added 77% to the insert's WAL and about doubled its time. They also took 47.7 MiB next to a 104.2 MiB table.

The status index is the smallest of the three (6.2 MiB after the load) and does the most damage on updates. PostgreSQL skips new index entries with a heap-only tuple update only when "the update does not modify any columns referenced by the table's indexes". Index status, and every status change writes to every index on the table. Find the tables where this happens:

SELECT relname, n_tup_upd, n_tup_hot_upd,
       round(100.0 * n_tup_hot_upd / nullif(n_tup_upd, 0), 1) AS hot_pct
FROM pg_stat_user_tables
ORDER BY n_tup_upd DESC
LIMIT 20;

A low hot_pct on a busy table, next to an unused index on a column the application updates, is the index to drop first. The dollars are indirect on RDS and Cloud SQL: the extra WAL goes to every standby and replica and uses I/O and CPU that set the instance size. On Aurora PostgreSQL Standard they are direct, since I/O bills at $0.20 per million requests.

Replicas, snapshots and non-production copies: three checks

A replica's cost is the whole instance plus its own volume. Deleting the reader-less replica removes $328.50 of compute and $57.50 of gp3. On Cloud SQL, a read replica also needs at least as much storage as its primary, so a replica copies the primary's unshrunk volume too.

Snapshots bill only above the allowance. The estate keeps 1,500 GB of manual snapshots from an old migration, but only 1,100 GB of backup storage is charged, so deleting all of them saves $104.50, not 1,500 GB times $0.095 ($142.50).

Staging copies of production carry production's volume. Each RDS staging instance bills $57.50 a month of gp3 whether it runs or not. A schedule cuts its compute, not its storage.

Rank the waste in dollars, then cut the reversible steps first

Bar chart of monthly waste by class in a hypothetical estate of 8 RDS and 4 Cloud SQL Postgres instances costing $3,729.45 a month. Staging and dev running 24/7: $437.72. Read replica with no readers: $386.00. Oversized production primary: $327.77. Old manual snapshots: $104.50. Unused indexes and unread tables: $0.00 on the bill, $51.75 after a storage shrink.

Waste classHow to measureLine it movesMonthlyRisk of cutting
Non-production on 24/7connections by hour, 2 weekscompute$437.72low: a schedule is undone by starting the instance
Replica with no readersconnections and table counters on the replicacompute and storage$386.00medium: snapshot first, a new replica takes time to build
Oversized primaryCPU and memory percentiles, 28 dayscompute$327.77medium: size back up if the peak returns
Old manual snapshotssnapshot list against the allowancebackup storage$104.50high: a deleted snapshot is gone
Unused indexes and tablesidx_scan, last reads, a full cyclewrites now, storage after a shrink$0.00medium: recreate the index, restore the table

The five add up to $1,255.99, a third of this estate's bill, on assumptions we chose. Your counts will differ. The order usually will not, because compute is 80% of the bill.

Suppose each RDS production primary held 90 GB of unused indexes and unread tables. Dropping them frees 180 GB and $0.00, since the volumes stay at 500 GB. Shrinking them takes a Blue/Green deployment; on Cloud SQL the same step is a storage shrink, which Google says requires "instance downtime". If both primaries and the one remaining replica ended up 90 GB smaller, the most it could save is 90 × ($0.23 × 2 + $0.115) = $51.75 a month. On Aurora Standard, which bills consumed storage at $0.10 per GB-month, AWS says you "can reduce storage charges by dropping tables, indexes, databases". The same 180 GB would save $18.00 at once. Our Aurora vs RDS comparison covers when that matters.

The strongest objection: dropping an unused index takes one statement, so why wait? Because the risk is real and the dollars are small. Reading idx_scan = 0 on the primary alone misses indexes that only replica queries use, and a month-end job finds the gap. Drop write-heavy unused indexes for their write cost, after a full cycle of readings on every server. Schedule the volume shrink for the day the freed space is large enough to pay for the switchover.

What we did not test: RDS or Cloud SQL themselves, MySQL, Aurora's I/O count for this workload, and larger tables, where checkpoints and full-page writes change the WAL ratios. The estate and its waste are assumptions chosen to show the method.

Datapace is building one map of your data estate (databases, warehouses, pipelines and what consumes them) with usage and cost attached, and agents that propose changes as pull requests your team reviews and merges. If you want to rank the waste in one engine first, start with RDS cost optimization; to price the same Postgres across clouds, read cloud database cost.

Sources

  1. AWS Price List API, RDS offer file for US East, published November 13, 2026, read November 14, 2026 (RDS for PostgreSQL instances, gp3, backup storage; Aurora PostgreSQL storage and I/O).
  2. Google Cloud, Cloud SQL pricing, Enterprise edition, Iowa (us-central1), read November 14, 2026.
  3. AWS, Amazon RDS for PostgreSQL pricing (backup storage allowance) and Working with read replicas.
  4. AWS, Increasing DB instance storage capacity and Amazon RDS Blue/Green Deployments support storage volume shrink, November 20, 2024.
  5. AWS, Amazon Aurora storage.
  6. Google Cloud, Instance settings, About storage shrink and Start, stop and restart instances.
  7. PostgreSQL 18 documentation, The cumulative statistics system and Heap-only tuples.