Understanding YSQL Catalog Caching in YugabyteDB

Distributed databases change the cost model of metadata access. In PostgreSQL, system catalog tables live locally on disk and are typically cached by the operating system. Metadata reads are essentially free.

In YugabyteDB, YSQL catalog tables are stored in DocDB and replicated via Raft. Accessing them may involve an RPC to the master leader hosting the SysCatalog tablet.

To maintain performance, YugabyteDB relies on several caching layers between the Postgres backend and the catalog tablet.

Understanding these layers helps explain:

  • ● why some queries generate catalog RPCs

  • ● why query frequency affects catalog performance

  • ● why bursts of DDL can invalidate caches

  • ● how catalog prefetching improves performance

💡 Key Insight
In PostgreSQL, catalog reads are local file reads. In YugabyteDB, catalog tables are distributed objects stored in DocDB. Without caching, many metadata lookups would require RPCs to the master leader hosting the SysCatalog tablet. Multiple caching layers ensure that, once warmed, most catalog lookups are served entirely from backend memory.

What Lives in the System Catalog

PostgreSQL/YSQL stores all metadata in system catalog tables.

These tables describe every object in the database.

Table Purpose
pg_class Tables, indexes, sequences
pg_attribute Column definitions
pg_index Index metadata
pg_type Data type definitions
pg_proc Functions and procedures
pg_operator Operators
pg_namespace Schemas
pg_constraint Constraints
pg_statistic Optimizer statistics

Every query depends on these tables.

  • ● Parsing needs operator and type definitions

  • ● Planning needs statistics and index metadata

  • ● Execution needs table schema details

Where Catalog Data Lives in YugabyteDB

All YSQL catalog tables are stored in a single tablet:

				
					SysCatalogTablet
				
			

Key characteristics:

Property Value
Storage DocDB
Replication Raft
Location Masters
Leader One master node

Even though a query runs on a tserver, catalog reads ultimately route to the master leader hosting the SysCatalog tablet.

Inspecting the SysCatalog Tablet

Although YSQL exposes catalog relations such as pg_class, pg_attribute, and pg_proc as separate PostgreSQL system tables, their metadata is stored internally in a special system table called sys.catalog.

You can inspect this internal catalog container with yb-admin:

				
					yb-admin --init_master_addrs <master-ip> list_tables include_table_id | grep sys.catalog
				
			

Example output:

				
					system_schema.sys.catalog [sys.catalog.uuid] sys.catalog.uuid
				
			

This shows the internal table ID for the system catalog container.

Next, list its tablets:

				
					yb-admin --init_master_addrs <master-ip> list_tablets tableid.sys.catalog.uuid 0 include_followers
				
			

Example output:

				
					Tablet-UUID                      	Range                                                    	Leader-IP       	Leader-UUID 	                    Followers
00000000000000000000000000000000 	range: [<start>, <end>)                                  	127.0.0.1:7100  	ed09abb40da4401a9fe4137b94f6ff3f 	127.0.0.2:7100,127.0.0.3:7100
				
			
The important detail is the tablet ID:
  • 00000000000000000000000000000000

This is the SysCatalog tablet, which stores YSQL catalog metadata for tables such as pg_class, pg_attribute, and pg_proc.

A natural question is whether you can prove this from ysqlsh using yb_local_tablets. In practice, that does not work for YSQL catalog tables the way it does for normal user tables. These catalog relations are exposed logically through YSQL, but their underlying storage lives in the special internal sys.catalog table.

So the most reliable way to inspect the shared underlying storage is through yb-admin, not yb_local_tablets.

Because many logical catalog tables ultimately resolve to this one internal tablet, YugabyteDB relies heavily on catalog caching to avoid repeated RPCs to the SysCatalog tablet leader.

Catalog Read Path

When a backend needs catalog metadata, the request flows through several layers.

Postgres Backend pggate YSQL ↔ DocDB bridge PgClientService Tablet Server Master SysCatalog Tablet Leader

Catalog reads originate in the Postgres backend but are ultimately served by the SysCatalog tablet leader on the master. Because catalog metadata lives in this distributed tablet, YugabyteDB relies heavily on caching layers to minimize repeated RPCs.

Without caching, a single query could trigger multiple catalog RPCs.

The Most Important Cache: Catcache

Each Postgres backend maintains a catalog cache (catcache).

Once a catalog entry is loaded:

				
					lookup key → cached catalog row
				
			

Future lookups are served directly from backend memory.

Important characteristics:

Property Behavior
Scope Per backend
Shared across sessions No
Persistence Lifetime of backend process

Because it is not shared, each backend must warm its own catalog cache.

Why Query Frequency Matters

Because catcache is per-backend, workload patterns matter.

Frequently executed queries… These warm the catalog cache quickly.

				
					catalog lookup → catcache hit
				
			

No RPCs are required.

Rare queries often run on cold backends.

This causes:

				
					catcache miss → catalog RPC
				
			

Examples include:

  • ● administrative queries

  • ● rarely used joins

  • ● low-traffic endpoints

If you’d like to observe catalog cache behavior in practice, including which catalog tables are accessed during catcache misses, see this related YugabyteDB Tip:

Syscache Coverage

YugabyteDB uses PostgreSQL’s syscache (system catalog cache) infrastructure. Each syscache ID corresponds to a catalog lookup pattern.

There are 85 syscache IDs (0–84) that cover most catalog tables and their lookup keys.

Syscache Lookup
RELOID Table by OID
RELNAMENSP Table by name + schema
ATTNUM Column by table + attribute number
TYPEOID Type by OID
PROCNAMEARGSNSP Function by name + argument types

Each syscache entry stores:

				
					lookup key → catalog tuple
				
			

Why Negative Caching Is Restricted

PostgreSQL normally caches negative lookups.

Example:

				
					lookup table "foo"
table doesn't exist
→ negative entry stored
				
			

Future lookups skip the catalog scan.

However, in YugabyteDB this behavior is restricted.

Negative caching is disabled for most caches because not all DDL operations increment the catalog version.

For example:

				
					CREATE TABLE new_table;
				
			

Negative caching is disabled for most caches because not all DDL operations increment the catalog version.

For example, creating a new standalone table adds a new entry to the catalog but historically did not always trigger a global catalog version bump. Without a version bump, a backend that previously cached “table does not exist” could continue believing the table is missing even after it was created.

Without that bump, a backend that cached:

				
					table new_table does not exist
				
			

would continue believing the table does not exist even after it was created.

Because the invalidation mechanism cannot reliably clear these entries, most negative caching is disabled.

Negative caching is only allowed for specific syscache IDs or those listed in:

				
					yb_neg_catcache_ids
				
			
You can inspect the current value of the GUC yb_neg_catcache_ids via the pg_settings catalog table:
				
					SELECT setting, short_desc FROM pg_settings WHERE name = 'yb_neg_catcache_ids';
				
			

Example output:

				
					.setting |                                         short_desc
---------+--------------------------------------------------------------------------------------------
         | Comma separated list of additional sys cache ids that are allowed to be negatively cached.
				
			

Catalog Version and Cache Invalidation

YugabyteDB tracks catalog updates using a catalog version number.

When DDL modifies metadata:

				
					master increments catalog version
				
			

Tservers learn the new version via heartbeat.

Before executing a statement, each backend compares:

				
					backend version
vs
shared catalog version
				
			

If they differ, the backend refreshes its caches.

Incremental vs Full Cache Refresh

Earlier YugabyteDB versions sometimes triggered full catalog refreshes after a version mismatch.

With YugabyteDB 2025.1, the behavior improved significantly.

⚙️ Behavior Change in 2025.1
Incremental catalog refreshes are now the normal path. Full catalog cache refreshes are much less likely, reducing the chance of bursts of metadata reads hitting the master leader.

Typical refresh flow:

  • 1. DDL occurs
  • 2. Master bumps catalog version
  • 3, Tserver updates shared version
  • 4. Backend detects version mismatch
  • 5. Invalidation messages are processed
  • 6. Specific catcache/relcache entries are invalidated
  • 7. Backend re-reads the invalidated catalog data if needed

Statement-Level Snapshot Consistency

Catalog reads also carry a timestamp to ensure consistency.

The field:

				
					catalog_read_time
				
			

is returned in:

				
					PgPerformResponsePB
				
			

The backend uses this value to maintain a consistent metadata snapshot within a single SQL statement.

This prevents a statement from observing partially applied metadata changes during concurrent DDL.

Catalog Prefetching

YugabyteDB includes a catalog prefetcher.

This mechanism loads many catalog tables in a single batched RPC.

Prefetching runs during:

  • 1. Connection initialization

  • 2. Full catalog refresh

For a practical method to discover which catalog tables should be preloaded, see this YugabyteDB Tip:

Tserver Response Cache

Tservers maintain a shared response cache.

However, this cache currently applies mainly to full catalog refresh operations, not individual catalog lookups.

Scenario Response Cache Used
Full catalog refresh Yes
Individual catcache miss Usually no

This cache helps prevent multiple backends from triggering identical bulk catalog reads during refresh events.

Practical Takeaways

Catalog caching in YugabyteDB involves multiple layers:

Layer Scope
Catcache Per backend
Relcache Per backend
Shared catalog version Per tserver
Catalog prefetcher Bulk catalog loads
Tserver response cache Shared per node

In steady state:

  • catalog lookups → memory

RPCs typically occur only when:

  • ● a backend encounters a cold cache

  • ● a rare query accesses uncached metadata

  • ● DDL invalidates catalog entries

Understanding these behaviors helps explain catalog traffic patterns when diagnosing performance issues.

Have Fun!