"Cache your database queries" is advice given without qualification, and it skips something important: the database is already .

A relational database keeps recently read pages in a buffer pool, normally gigabytes of it. After the first execution a repeated query usually touches no disk at all, so an application cache in front of it is not saving a disk read.
That changes what to measure. Comparing a cache against a cold database makes every query look worth caching, because you are measuring a disk read the database would have eliminated on its own.
The script is at scripts/labs/querycache/whats_left.py. It loads 300,000 rows into a real PostgreSQL, warms the buffer pool deliberately, and then measures four queries with EXPLAIN (ANALYZE, BUFFERS).

After three executions the database serves all of these without touching disk. What an application cache would save is the server column, and it ranges from 0.03 milliseconds to 12.91.

The spread is 430 times, across four queries on one table in one database. advice that treats them the same is advice about the wrong thing, because what a cache saves is the height of these bars and nothing else.

Run EXPLAIN (ANALYZE, BUFFERS) before deciding to cache a query. If the read count is already zero on a warm system, the disk argument for caching it does not exist.

Caching the cheapest one replaces 0.03 milliseconds of work with a network round trip, which the previous lesson measured at 17 microseconds on loopback and far more in production. Most teams cache the query they call most often, which is usually the key lookup.

This is why an aggregate is so effective and caching a lookup is not. The aggregate's cost is proportional to how much data it touches, so it grows as the table grows, and the cache turns a growing cost into a constant one.

Every write had to invalidate every cached result touching the modified table, which required a global lock, and on a busy server that lock cost more than the cache saved. The buffer pool survives everywhere because caching pages has no such problem.


A cache in front of a sequential scan hides a missing index, performs beautifully in normal conditions, and delivers the unindexed query to your database at full traffic the moment the cache restarts.

For an expensive aggregate allowed to be minutes old, this replaces the cache, the , the invalidation and the extra dependency with one scheduled statement, and it survives a restart.


A query cache key must contain every input that changes the answer. Row level security, default schemas and session variables are all invisible in the text and all change the result.

A search result storing whole rows is invalidated by any change to any of them. A list of ids is only invalidated when the membership changes, and each row is cached under its own key shared by every query that returns it.


Every step before it is cheaper, permanent and has no staleness. The queries that reach the cache are the aggregates and reports, which is exactly the category the measurement showed a cache helps by hundreds of times.

A query taking 2 milliseconds ten thousand times a minute costs more than one taking 900 milliseconds once, and only one of them gets complained about.
4 questions - Score 80% to pass
You run EXPLAIN (ANALYZE, BUFFERS) on a repeated query and see 3,093 shared hits and 0 shared reads. What does adding a cache in front of it save?
On the measured table, a primary key lookup took 0.03 ms warm and a group-and-sort took 12.91 ms. Which is the better caching candidate?
Why did MySQL remove its query result cache in 8.0?
A query is slow and EXPLAIN shows a sequential scan with a non-zero shared read count. What should you do first?
Query results are much harder to invalidate than objects, because a result is derived from many rows and a write only tells you about one. This is the main reason query caching in practice means a .
The bottom right branch is the one people skip. A cache in front of a query that needed an index makes the problem invisible and permanent, and it reappears the moment the cache misses.
The last row is worth looking for deliberately. Reports over completed periods, historical aggregates and anything keyed by a finished date are permanently cacheable, and they are often the most expensive queries in the system.

Size for state two and know what state four looks like. Most incidents are a system designed for the first bar meeting the third or fourth, and the difference is not a factor of two.

That ranking is the list you actually want, and it sorts the candidates without any guessing. A materialised view is worth considering before an external cache for pure aggregates.

The second line is where most effort is wasted. Teams cache the query in the hottest loop, which is nearly always a primary key lookup.

This lesson exists because the standard advice is aimed at a cost the database already eliminated. The useful version is narrower and much more effective: find the queries that compute rather than read, and cache those.