Databases 10 min read

How PostgreSQL 18 Made My Redis Cache Redundant (And What You Can Learn)

After disabling Redis in production and carefully testing on PostgreSQL 18, the author reduced p95 latency, simplified the system, and lowered operational overhead by replacing the cache with a covering index, a generated column, and a materialized view, while providing detailed query examples, configuration tweaks, and performance measurements.

dbaplus Community
dbaplus Community
dbaplus Community
How PostgreSQL 18 Made My Redis Cache Redundant (And What You Can Learn)

1. The Day I Flipped the Switch

The author shut down Redis in a production environment without rolling back, describing the lingering anxiety because caches feel like a safety belt. After cautious testing on PostgreSQL 18, the 95th‑percentile latency (p95) dropped, the system became simpler, and on‑call burden was reduced.

2. What Happened Inside PostgreSQL

Two concrete changes made the cache unnecessary:

Coverage and Selectivity : A covering index combined with a generated column allowed the optimizer to fetch only the required fields, avoiding heap scans.

Pre‑computed Shape : A materialized view with concurrent refresh handled the expensive aggregation that the cache previously hid.

PostgreSQL 18’s smarter execution plans and predictable I/O completed the rest of the work.

3. The Query That Rendered Redis Superfluous

The hot path is a parameterized product query with a bit of personalization. The author performed an end‑to‑end analysis and switched to a direct database solution using a tighter index and a pre‑computed projection.

-- Schema and plan helpers (run in a maintenance window)
CREATE TABLE catalog_item (
  item_id BIGSERIAL PRIMARY KEY,
  category_id BIGINT NOT NULL,
  tenant_id BIGINT NOT NULL,
  price_cents INTEGER NOT NULL,
  rating_avg NUMERIC(3,2) NOT NULL DEFAULT 0.0,
  tags TEXT[] NOT NULL DEFAULT '{}',
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  deleted BOOLEAN NOT NULL DEFAULT FALSE
);

-- Generated column to pre‑compute a simple personalization bucket
ALTER TABLE catalog_item
  ADD COLUMN p_bucket SMALLINT GENERATED ALWAYS AS (
    (rating_avg * 10)::smallint
  ) STORED;

-- Covering index that matches the query (note the INCLUDE list)
CREATE INDEX CONCURRENTLY idx_catalog_lookup
  ON catalog_item (tenant_id, category_id, p_bucket, deleted)
  WHERE deleted = FALSE
  INCLUDE (item_id, price_cents, rating_avg, updated_at, tags);

-- Materialized view for "trending" that used to be cached
CREATE MATERIALIZED VIEW mv_trending AS
SELECT tenant_id, category_id,
       item_id, price_cents, rating_avg, tags,
       row_number() OVER (PARTITION BY tenant_id, category_id ORDER BY rating_avg DESC, updated_at DESC) AS rk
FROM catalog_item
WHERE deleted = FALSE;

-- Keep it fresh without blocking writers
CREATE UNIQUE INDEX CONCURRENTLY mv_trending_pk
  ON mv_trending (tenant_id, category_id, item_id);

-- In a job runner or cron:
-- REFRESH MATERIALIZED VIEW CONCURRENTLY mv_trending;

Application calls now become a single round‑trip:

-- Serve top N without touching Redis
PREPARE fetch_slice (bigint, bigint, smallint, int) AS
SELECT item_id, price_cents, rating_avg, tags
FROM catalog_item
WHERE tenant_id = $1
  AND category_id = $2
  AND p_bucket >= $3
  AND deleted = FALSE
ORDER BY rating_avg DESC, updated_at DESC
LIMIT $4;

-- When trending is requested
PREPARE fetch_trending (bigint, bigint, int) AS
SELECT item_id, price_cents, rating_avg, tags
FROM mv_trending
WHERE tenant_id = $1 AND category_id = $2 AND rk <= $3;

4. Tuning PostgreSQL for Low Latency

Two modest configuration tweaks helped keep I/O stable and let the planner reliably use indexes and parallelism:

# postgresql.conf (16 GB VM example)
shared_buffers = '4GB'
effective_cache_size = '11GB'
work_mem = '64MB'
maintenance_work_mem = '1GB'
track_io_timing = on
jit = on
max_worker_processes = 8
max_parallel_workers_per_gather = 2
random_page_cost = 1.1
default_statistics_target = 500

On faster NVMe storage, lowering random_page_cost keeps index scans attractive, while track_io_timing warns when disk performance assumptions are wrong.

5. How the Author Measured the Impact

A simple client exercised the usual cached parameters. After three warm‑up runs, median latencies were recorded. The results showed that network hops and serialization overhead were larger than expected.

| Path                                 |  p50 |  p95 |  p99 | Notes                                 |
|--------------------------------------|------|------|------|---------------------------------------|
| Redis (hit)                          |  6 ms| 18 ms| 28 ms| fast but extra hop                    |
| Redis (miss → DB)                    | 24 ms| 80 ms|120 ms| hop + serialization + origin          |
| Postgres 18 direct (covering index)  | 18 ms| 55 ms| 95 ms| fewer hops, stable tail               |
| Postgres 18 via mv_trending (warm)   | 12 ms| 38 ms| 70 ms| precomputed hot slice                 |

On the feed endpoint, the direct database path beat the cache’s tail latency and eliminated the cliff between hit and miss, a gap that previously caused support tickets.

6. When a Cache Still Makes Sense

The author does not oppose caching; it remains valuable for cross‑request coordination, rate‑limiting, or fan‑out/fan‑in across services. Cache was removed only where it masked planner errors and added variance. An attempted synchronous_commit tweak was rolled back because the risk outweighed the benefit.

7. Reusable Changes for Other Teams

Two application‑level adjustments were kept after the experiment:

Use stable‑order prepared statements (SQL prepared statements) instead of ORM‑generated queries, ensuring stable plans and fast parsing.

Refresh the materialized view regularly (every few minutes) with CONCURRENTLY to keep reads smooth without complex cron tricks.

8. Takeaways for Your Team

If your cache only hides a slow source, fix the source first. Cover the query fully, pre‑compute only the truly expensive parts, and measure p95 rather than averages. When a direct database path can beat cache miss latency and reduce operational pain, let the simpler architecture win.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

indexingredisDatabase OptimizationPostgreSQLmaterialized view
dbaplus Community
Written by

dbaplus Community

Enterprise-level professional community for Database, BigData, and AIOps. Daily original articles, weekly online tech talks, monthly offline salons, and quarterly XCOPS&DAMS conferences—delivered by industry experts.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.