Query Every YSQL Node at Once with Cluster-Wide Database Views

Many PostgreSQL statistics views are local to the PostgreSQL server process you are connected to.

That matters in YugabyteDB because a cluster can have many YB-TServers, each running its own YSQL server.

For example, if a CREATE INDEX command was submitted to node 3, querying:

				
					SELECT *
FROM pg_stat_progress_create_index;
				
			

from node 1 won’t show you that index build.

Historically, if you didn’t know which YSQL node was handling the work, you had to query every node yourself.

Starting with YugabyteDB 2026.1.2, there’s a much easier way:

				
					SELECT *
FROM gv$pg_stat_progress_create_index;
				
			

Cluster-wide database views provide cluster-wide counterparts for a growing set of PostgreSQL and YugabyteDB statistics views. Each result includes a server_uuid identifying the YB-TServer that supplied the row.

And yes…

That little gv$ prefix can save a lot of work. 😎

⚠️ Tech Preview
Cluster-wide database views are currently a Tech Preview feature and are available in YugabyteDB 2026.1.2 and later.
The feature is disabled by default and must be enabled using yb_enable_global_views.
🎥 YugabyteDB Friday Tech Talk
Want to see cluster-wide database views explained and demonstrated in more detail?
This YugabyteDB Friday Tech Talk walks through the feature, how it works, and how it can simplify cluster-wide observability.
Presented by: Abhinab Saha, Software Engineer, and Hemant Bhanawat, Senior Director of Engineering

We’ve Been Here Before

This isn’t a new problem.

I previously published two YugabyteDB Tips showing different ways to gather node-local catalog and statistics data across an entire cluster.

🕰️ The Original Approach: Build the FDWs Yourself
In an earlier Tip, I showed how to use postgres_fdw, foreign servers, user mappings, and a consolidated view to query node-local catalog data from one place.

That Tip specifically used pg_stat_progress_create_index as an example of information that was visible only on the YSQL node where the CREATE INDEX command was initiated.

Later, I took a different approach.

🛠️ The Next Evolution: Query Every Node Automatically
I later created a shell script that discovers the YB-TServers, runs the same SQL against every node in parallel, and aggregates the results.

That script is still useful when you need to execute arbitrary SQL against every YSQL server.

But for supported statistics views, YugabyteDB can now do the aggregation for us.

2024
Manual postgres_fdw setup
↓
2025
Script queries every YSQL node
↓
2026
SELECT ... FROM gv$...

How Cluster-Wide Views Work

For supported views, YugabyteDB creates a gv$ counterpart in pg_catalog.

For example:

Local View Cluster-Wide View Useful For
pg_stat_activity gv$pg_stat_activity Sessions and running SQL across all nodes
pg_stat_statements gv$pg_stat_statements Query statistics across the cluster
pg_stat_progress_create_index gv$pg_stat_progress_create_index Index builds running on any YSQL node
yb_active_session_history gv$yb_active_session_history ASH analysis across all YB-TServers
pg_stat_progress_analyze gv$pg_stat_progress_analyze ANALYZE progress cluster-wide

The implementation is actually pretty interesting.

Cluster-wide views are built using YugabyteDB’s postgres_fdw infrastructure. When you query a gv$ view, the planner creates a scan for each live YB-TServer, each node evaluates its local view, and the coordinating node combines the results. YugabyteDB adds server_uuid to identify where each row came from.

In other words, YugabyteDB is now doing much of the FDW plumbing that we previously had to create ourselves!

Build a Three-Node Demo Cluster

Let’s see it in action.

We’ll create a three-node local cluster using yugabyted.

Start node 1:

				
					./bin/yugabyted start \
  --advertise_address=127.0.0.1 \
  --base_dir=${HOME}/var/node1 \
  --cloud_location=aws.us-east-2.us-east-2a
				
			

Start node 2:

				
					./bin/yugabyted start \
  --advertise_address=127.0.0.2 \
  --base_dir=${HOME}/var/node2 \
  --cloud_location=aws.us-east-2.us-east-2b \
  --join=127.0.0.1
				
			

Start node 3:

				
					./bin/yugabyted start \
  --advertise_address=127.0.0.3 \
  --base_dir=${HOME}/var/node3 \
  --cloud_location=aws.us-east-2.us-east-2c \
  --join=127.0.0.1
				
			

Then configure zone fault tolerance:

				
					./bin/yugabyted configure data_placement \
  --base_dir=${HOME}/var/node1 \
  --fault_tolerance=zone
				
			

This follows YugabyteDB’s documented local three-node yugabyted setup. When the third node joins, yugabyted automatically configures replication factor 3.

🍎 Running on macOS?
Before starting nodes 2 and 3, add the additional loopback addresses:
sudo ifconfig lo0 alias 127.0.0.2
sudo ifconfig lo0 alias 127.0.0.3

Verify the cluster:

				
					./bin/ysqlsh -h 127.0.0.1 -c "
SELECT
    host,
    region,
    zone,
    uuid
FROM yb_servers()
ORDER BY host;"
				
			

You should see all three YB-TServers.

Enable Cluster-Wide Views

The feature is controlled by:

				
					yb_enable_global_views
				
			

For a simple demo, enable it in your session:

				
					SET yb_enable_global_views = on;
				
			

You can also enable it cluster-wide using:

				
					--ysql_pg_conf_csv=yb_enable_global_views=true
				
			

or configure it for a dedicated monitoring role. Querying the views requires membership in pg_read_all_stats; superusers already qualify.

See Which gv$ Views Are Available

Instead of memorizing them:

				
					SELECT relname AS view_name
FROM pg_class
WHERE relnamespace = 'pg_catalog'::regnamespace
  AND relkind = 'f'
  AND relname LIKE 'gv$%'
ORDER BY 1;
				
			

On a current release, you’ll see objects such as:

				
					gv$pg_stat_activity
gv$pg_stat_all_indexes
gv$pg_stat_all_tables
gv$pg_stat_database
gv$pg_stat_progress_analyze
gv$pg_stat_progress_copy
gv$pg_stat_progress_create_index
gv$pg_stat_statements
gv$yb_active_session_history
...
				
			

The exact set can depend on which extensions are installed. For example, the cluster-wide pg_stat_statements view depends on the underlying pg_stat_statements extension.

Demo: Find an Index Build No Matter Which Node Started It

This is where the feature gets fun.

YugabyteDB’s normal pg_stat_progress_create_index view is node-local. It only contains CREATE INDEX operations started by clients connected to that YSQL server.

Let’s prove it.

Create Some Demo Data

Connect to node 1:

				
					./bin/ysqlsh -h 127.0.0.1 -d yugabyte
				
			

Create a table:

				
					CREATE TABLE demo_orders (
    id           bigint PRIMARY KEY,
    customer_id  bigint NOT NULL,
    payload      text
) SPLIT INTO 6 TABLETS;
				
			

Load enough data that the index backfill remains visible for a while:

				
					INSERT INTO demo_orders (id, customer_id, payload)
SELECT
    g,
    g % 100000,
    repeat(md5(g::text), 4)
FROM generate_series(1, 5000000) AS g;
				
			

Then gather statistics:

				
					ANALYZE demo_orders;
				
			
💡 Need More Time to Catch the Backfill?
Index creation can be surprisingly fast on a laptop. If the index disappears before you can observe it, increase the number of rows in the demo table.

Terminal 1: Watch From Node 1

Connect to node 1:

				
					./bin/ysqlsh -h 127.0.0.1 -d yugabyte
				
			

Run:

				
					SELECT
    tbl.relname AS table_name,
    idx.relname AS index_name,
    command,
    phase,
    tuples_total,
    tuples_done
FROM pg_stat_progress_create_index p
JOIN pg_class tbl
  ON tbl.oid = p.relid
JOIN pg_class idx
  ON idx.oid = p.index_relid
WHERE tbl.relname = 'demo_orders';

\watch 1
				
			

At the moment, there should be nothing to see.

Terminal 2: Start the Index From Node 3

Now connect to node 3:

				
					./bin/ysqlsh -h 127.0.0.3 -d yugabyte
				
			

Create the index:

				
					CREATE INDEX demo_orders_customer_idx
ON demo_orders(customer_id);
				
			

YugabyteDB performs online index backfill by default for supported index builds.

Now look back at the pg_stat_progress_create_index query on node 1.

Still nothing.

Why?

Because the CREATE INDEX command was issued through node 3.

That local view on node 1 doesn’t know about it.

Now Use the Global View

Stop the \watch and enable global views on node 1:

				
					SET yb_enable_global_views = on;
				
			

Now run:

				
					SELECT
    p.server_uuid,
    p.datname,
    tbl.relname AS table_name,
    idx.relname AS index_name,
    p.command,
    p.phase,
    p.tuples_total,
    p.tuples_done
FROM gv$pg_stat_progress_create_index p
JOIN pg_class tbl
  ON tbl.oid = p.relid
JOIN pg_class idx
  ON idx.oid = p.index_relid
WHERE tbl.relname = 'demo_orders';
				
			

There it is. 🎯

The same session on node 1 can now see the index build that was initiated through node 3.

Make the UUID Human-Friendly

server_uuid tells us which YB-TServer supplied the row.

That’s useful, but a UUID isn’t the friendliest thing to stare at.

Let’s join it to yb_servers():

				
					SELECT
    s.host,
    s.region,
    s.zone,
    p.server_uuid,
    tbl.relname AS table_name,
    idx.relname AS index_name,
    p.phase,
    p.tuples_total,
    p.tuples_done,
    CASE
        WHEN p.tuples_total > 0
        THEN ROUND(
            100.0 * p.tuples_done / p.tuples_total,
            1
        )
    END AS pct_done
FROM gv$pg_stat_progress_create_index p
JOIN yb_servers() s
  ON s.uuid::uuid = p.server_uuid
JOIN pg_class tbl
  ON tbl.oid = p.relid
JOIN pg_class idx
  ON idx.oid = p.index_relid
WHERE tbl.relname = 'demo_orders';
				
			

Now we can answer not only:

  • Is an index being built?

but:

  • Which YSQL node initiated it, and where is that node located?

For our demo, that should identify:

				
					127.0.0.3
aws.us-east-2.us-east-2c
				
			

because that’s the node where we submitted the CREATE INDEX.

🔥 This Is the Part I Like
You no longer have to know which YSQL node originally received the CREATE INDEX.
Connect to any node, query gv$pg_stat_progress_create_index, and YugabyteDB gathers the progress information from across the cluster for you.

When the index finishes, its row disappears from pg_stat_progress_create_index, which is the documented behavior of the progress view.

Bonus #1: Are My Client Connections Actually Balanced?

Here’s another use I really like.

A load balancer, Smart Driver, or YSQL Connection Manager may be expected to distribute sessions across YSQL nodes.

Instead of logging into each node to check, ask the cluster:

				
					SELECT
    s.host,
    s.region,
    s.zone,
    a.application_name,
    COUNT(*) AS sessions,
    COUNT(*) FILTER (
        WHERE a.state = 'active'
    ) AS active_sessions
FROM gv$pg_stat_activity a
JOIN yb_servers() s
  ON s.uuid::uuid = a.server_uuid
WHERE a.backend_type = 'client backend'
GROUP BY
    s.host,
    s.region,
    s.zone,
    a.application_name
ORDER BY
    sessions DESC;
				
			

Now you’ve got a simple cluster-wide connection distribution report.

This builds on the documented “busiest node” example by adding the actual host, region, zone, and application name. The documented examples show grouping gv$pg_stat_activity by server_uuid; joining to yb_servers() makes that output much more operationally useful.

That could be especially handy when validating:

  • ● Application connection distribution
  • ● Load balancer behavior
  • ● Smart Driver routing
  • ● YSQL Connection Manager usage
  • ● Uneven node utilization

Bonus #2: Find Query-Latency Skew Between Nodes

Here’s one that isn’t obvious at first.

gv$pg_stat_statements gives you the pg_stat_statements rows from every YSQL node.

The same normalized statement uses the same queryid across the cluster, so we can compare its execution statistics between nodes. YugabyteDB’s documentation specifically notes that queryid is consistent across nodes for the same statement.

That lets us look for a query that’s fast on one YSQL node but much slower on another:

				
					SELECT
    queryid,
    LEFT(MAX(query), 100) AS query,
    COUNT(*) AS nodes_seen,
    ROUND(MIN(mean_exec_time)::numeric, 2) AS fastest_node_ms,
    ROUND(MAX(mean_exec_time)::numeric, 2) AS slowest_node_ms,
    ROUND(
        (
            MAX(mean_exec_time) /
            NULLIF(MIN(mean_exec_time), 0)
        )::numeric,
        2
    ) AS node_skew_ratio
FROM gv$pg_stat_statements
WHERE calls >= 10
GROUP BY queryid
HAVING COUNT(*) > 1
ORDER BY node_skew_ratio DESC
LIMIT 10;
				
			

Now we’re asking something different from the normal “top queries” report:

  • Is the same SQL behaving differently depending on which YSQL node executes it?

A large node_skew_ratio could be a useful signal to investigate node-local differences such as resource pressure, connection behavior, cache state, or topology.

⚠️ Treat This as a Signal, Not a Diagnosis
Different per-node averages don’t automatically mean that something is wrong with a node. The workload mix, timing, number of executions, parameters, cache state, and other factors can all affect the measurements.
Use the result to identify interesting differences worth investigating.

Bonus #3: Where Is the Oldest Active Transaction?

Another useful cluster-wide operational query:

				
					SELECT
    s.host,
    s.region,
    s.zone,
    a.pid,
    a.usename,
    a.application_name,
    NOW() - a.xact_start AS transaction_age,
    LEFT(a.query, 120) AS query
FROM gv$pg_stat_activity a
JOIN yb_servers() s
  ON s.uuid::uuid = a.server_uuid
WHERE a.xact_start IS NOT NULL
  AND a.state <> 'idle'
ORDER BY a.xact_start
LIMIT 10;
				
			

Instead of asking every YSQL server:

  • “Do you have an old transaction?”

you ask once.

What Happens If a Node Is Down?

This behavior is important.

Suppose you have three nodes and one YB-TServer is unavailable when you issue a cluster-wide query.

YugabyteDB does not necessarily fail the entire query.

It returns rows from the nodes that responded and raises a WARNING for the node that could not be queried.

That means these two situations can otherwise look identical:

				
					Node returned zero rows
				
			

and:

				
					Node was never successfully queried
				
			

The warning is how you tell them apart.

⚠️ Cluster-Wide Does Not Always Mean Every Node Answered
When the result is important enough to act on, pay attention to WARNING messages.
A down, unreachable, or slow YB-TServer can be skipped while the query still returns data from the remaining nodes.

This is one of those details that matters a lot once a monitoring query becomes part of an operational workflow.

A Few Performance Considerations

Cluster-wide views are incredibly convenient, but they’re not free.

YugabyteDB currently executes one remote scan per selected YB-TServer, and the node scans execute sequentially. Each scan also opens a connection to the remote node for the duration of the query. The documentation therefore recommends treating cluster-wide views as monitoring/diagnostic tools rather than putting them on a hot application path.

A few useful rules:

Best Practice Why
Filter early with WHERE Eligible predicates can be pushed to each node, reducing returned rows
Filter by literal server_uuid when possible YugabyteDB can prune unneeded nodes from the plan entirely
Don’t assume LIMIT reduces remote work LIMIT is currently evaluated after matching rows arrive
Watch for warnings A missing node can otherwise look like a node with zero rows
Use these for observability, not application hot paths Cross-node queries are inherently more expensive than local views

These behaviors and limitations are documented for the Tech Preview implementation.

One Important Limitation of the New Approach

Cluster-wide database views don’t make the older techniques completely obsolete.

The built-in feature currently provides cluster-wide versions of a defined set of monitoring and statistics views.

Arbitrary SQL doesn’t automatically become:

				
					gv$anything_i_want
				
			

So if you need to run arbitrary SQL like:

				
					SELECT some_custom_expression
FROM some_node_local_source;
				
			

across every YSQL server, the shell-script approach can still be extremely useful.

✅ What it is
Built-in cluster-wide observability
❌ What it isn’t
A generic distributed SQL execution engine for every node-local query

That’s an important distinction.

Under the Hood: One Query, Multiple Foreign Scans

If you’re curious what YugabyteDB is doing, try:

				
					EXPLAIN (COSTS OFF)
SELECT
    server_uuid,
    pid,
    state,
    query
FROM gv$pg_stat_activity
WHERE state = 'active';
				
			

On a three-node cluster, the plan reflects the fact that YugabyteDB needs to query multiple YB-TServers and combine their results.

That’s also why the old FDW approach feels familiar: the built-in implementation itself uses YugabyteDB-specific postgres_fdw infrastructure.

Final Takeaway

For years, one of the small operational quirks of YSQL observability has been that many PostgreSQL statistics views are node-local.

If you didn’t know which YSQL server was handling something, you had to find it.

Over time, I’ve used a few different approaches to solve that problem:

First
Manual postgres_fdw setup
↓
Then
Scripts that query every YSQL node
↓
Now, for supported observability views
SELECT ... FROM gv$...

The index-backfill example makes the difference obvious.

Start a CREATE INDEX from node 3.

Connect to node 1.

This:

				
					SELECT *
FROM pg_stat_progress_create_index;
				
			

only sees CREATE INDEX operations initiated through node 1.

But this:

				
					SELECT *
FROM gv$pg_stat_progress_create_index;
				
			

can see the operation initiated through node 3, even though we’re connected to node 1.

And because every row includes server_uuid, we can join the result to yb_servers() to answer an even more useful question:

  • What’s happening across my YSQL cluster and exactly where is it happening?

That’s a pretty big improvement for three extra characters:

gv$

Have Fun!

On my best friend’s and my yearly visit to the Halloween store, we found Freddy Fazbear! From Five Nights at Freddy’s in 2014, to the movies, and now… a full-size Halloween decoration. 😂

I have to admit, he’d look pretty awesome in the new media room.

The only problem? I’m not sure how many times I could walk into a dark room, see this guy standing there, and remind myself, “It’s just a decoration… it’s just a decoration…” 😳🐻🎬