Skip the Intents: Faster Table Builds in YugabyteDB

YugabyteDB normally uses transaction intents for distributed writes. Rows are first written as provisional records and, when the transaction commits, those writes are applied to the main DocDB store.

But there is an interesting case where some of that work can be avoided.

If a transaction creates or rebuilds a relation and then populates it, YugabyteDB knows that the new relation is not following the normal write pattern. Starting with YugabyteDB 2026.1.2, an Early Access optimization can skip the provisional-write step and write qualifying data directly to the main store. The feature is enabled by default for qualifying standalone statements outside an explicit transaction block.

The setting controlling the feature is:

				
					yb_enable_new_relation_fastpath_write
				
			

And, as the tests below show, the difference can be substantial.

⚑ What Makes This Interesting?
This isn’t a query-planner optimization or a new SQL syntax. The SQL statement stays exactly the same.
YugabyteDB recognizes that the relation being populated was just created or rebuilt and can avoid part of the normal transactional write path.

The Normal Write Path vs. the Fast Path

In the normal transactional path, YugabyteDB stores provisional records in IntentsDB. Those records remain invisible until the transaction commits, after which the corresponding regular records are made visible in the main store.

The optimization removes that provisional-write step for qualifying operations.

Normal Transactional Write
Write Rows
↓
IntentsDB
↓
COMMIT
↓
RegularDB
↓
Intent Cleanup
New-Relation Fast Path
Write Rows
↓
RegularDB Directly
↓
COMMIT

The result is less write work and no commit-time intent cleanup for those writes. YugabyteDB describes this as removing roughly half of the write work for qualifying bulk operations.

Which Operations Can Benefit?

This isn’t a general-purpose optimization for every INSERT or UPDATE. It applies when the current transaction has created or rebuilt the destination relation.

OperationExample
Create and populate a tableCREATE TABLE AS or SELECT INTO
Rebuild a tableAdd/drop a primary key, change a column’s data type, or add a column with a volatile default
Refresh a materialized viewREFRESH MATERIALIZED VIEW without CONCURRENTLY
Load a table created earlier in the same transactionCOPY or INSERTΒ – requires the transaction-block form of the feature
Create an index on a table created in the same transactionCREATE INDEXΒ – requires the transaction-block form of the feature

The transaction-block form is currently Technology Preview and requires additional configuration, transactional DDL, and Read Committed isolation. The remainder of this Tip sticks to the simpler standalone-statement behavior.

How Do You Know It Actually Used the Fast Path?

Timing alone isn’t enough.

Each YB-TServer exports a counter named:

				
					skip_intents_writes
				
			

The counter increases whenever a batch of writes uses the skip-intents path. YugabyteDB specifically recommends sampling the counter before and after the operation.

For these tests I used a single-node YugabyteDB server so that the demonstration would be easy to follow.

Here is the small script I used:

				
					#!/usr/bin/env bash

HOST="127.0.0.1"
URL="http://${HOST}:9000/metrics"

value=$(
  curl -s "$URL" |
  grep -A1 '"name": "skip_intents_writes"' |
  grep '"value"' |
  sed -E 's/.*"value":[[:space:]]*([0-9]+).*/\1/'
)

if [[ -z "$value" ]]; then
  value="NOT FOUND"
fi

printf "\nskip_intents_writes snapshot\n"
printf "%-15s %20s\n" "YB-TServer" "skip_intents_writes"
printf "%-15s %20s\n" "---------------" "--------------------"
printf "%-15s %20s\n\n" "$HOST" "$value"
				
			

Example output:

				
					skip_intents_writes snapshot
YB-TServer       skip_intents_writes
--------------- --------------------
127.0.0.1                       1986
				
			
πŸ’‘ The Counter Is Cumulative
The absolute value isn’t what matters. Measure it immediately before and after the operation and compare the delta.
On a multi-node universe, check every YB-TServer because qualifying write batches may be handled by multiple servers.

I created a shell script named show_skip_intents.sh that I can call direclty in ysqlsh to show the value of skip_intents_writes.

For example:

				
					yugabyte=# \! ./show_skip_intents.sh

skip_intents_writes snapshot
YB-TServer       skip_intents_writes
--------------- --------------------
127.0.0.1                          0
				
			

Test Environment

The demos in this Tip were run on a single-node YugabyteDB cluster using YugabyteDB 2026.1.2.0.

Show nodes:

				
					SELECT host, cloud, region, zone
FROM yb_servers();
				
			

My result:

				
					.  host    | cloud  |   region    | zone
-----------+--------+-------------+-------
 127.0.0.1 | cloud1 | datacenter1 | rack1
(1 row)
				
			

And the YugabyteDB version:

				
					SELECT split_part(version(), '-', 3) AS yb_version;
				
			

I see:

				
					.yb_version
------------
 2026.1.2.0
(1 row)
				
			
πŸ§ͺ About These Benchmarks
All demos in this Tip were run on a single-node YugabyteDB 2026.1.2.0 cluster.
The goal is to make the behavior easy to reproduce and to clearly show the difference between the normal write path and the new-relation fast path.
These timings are illustrative rather than a universal performance expectation. Results will vary with node count, replication factor, hardware, storage, tablet distribution, row width, and cluster load.

Demo #1: CREATE TABLE AS

Let’s start with perhaps the cleanest example.

CREATE TABLE AS creates the relation and populates it in the same statement, making it a natural fit for the optimization.

First, create 5 million source rows with the optimization disabled so the setup itself doesn’t affect the test:

				
					SET yb_enable_new_relation_fastpath_write = off;

CREATE TABLE sales_orders AS
SELECT
    g AS order_id,
    g % 100000 AS customer_id,
    g % 1000 AS product_id,
    now() - (g % 365) * interval '1 day' AS order_ts,
    (g % 10000)::numeric / 100 AS amount
FROM generate_series(1, 5000000) AS g;
				
			

And enable timing:

				
					\timing on
				
			
Show the baseline value of yb_enable_new_relation_fastpath_write:
				
					yugabyte=# \! ./show_skip_intents.sh

skip_intents_writes snapshot
YB-TServer       skip_intents_writes
--------------- --------------------
127.0.0.1                          0
				
			

For this test I deliberately ran the fast-path test first, followed by the normal path.

Fast Path ON

				
					SET yb_enable_new_relation_fastpath_write = on;

CREATE TABLE sales_orders_ctas_on AS
SELECT *
FROM sales_orders;
				
			

Result:

				
					SELECT 5000000
Time: 138500.712 ms (02:18.501)
				
			
Show the value of yb_enable_new_relation_fastpath_write again:
				
					yugabyte=# \! ./show_skip_intents.sh

skip_intents_writes snapshot
YB-TServer       skip_intents_writes
--------------- --------------------
127.0.0.1                       4882
				
			

The metric changed from 0 to 4882.

Fast Path OFF

				
					SET yb_enable_new_relation_fastpath_write = off;

CREATE TABLE sales_orders_ctas_off AS
SELECT *
FROM sales_orders;
				
			

Result:

				
					SELECT 5000000
Time: 220228.459 ms (03:40.228)
				
			

What about the counter?

				
					yugabyte=# \! ./show_skip_intents.sh

skip_intents_writes snapshot
YB-TServer       skip_intents_writes
--------------- --------------------
127.0.0.1                       4882
				
			

It remained the same: 4882

πŸš€ About 37% Less Elapsed Time
Copying 5 million rows with CREATE TABLE AS took 138.501 seconds with the fast path enabled, compared with 220.228 seconds with it disabled.
That’s about 81.7 seconds less elapsed time, or a 37.1% reduction, in this test.
The skip_intents_writes counter tells the other half of the story: it increased from 0 to 4,882 during the fast-path run, then remained at 4,882 when the same operation was repeated with the fast path disabled.
That gives us both pieces of evidence: the operation completed substantially faster, and the YB-TServer metric confirms that the skip-intents write path was actually used.

Demo #2: Add a Primary Key

Here’s a less obvious example.

In YugabyteDB, adding a primary key requires a table rewrite. That makes ALTER TABLE ... ADD PRIMARY KEY a good operation to test with the new-relation fast path.

This can be especially relevant during migrations, where data may first be loaded into a table and the final primary key added afterward.

First, create a source table containing 2 million rows, along with two identical copies for the OFF and ON tests.

				
					SET yb_enable_new_relation_fastpath_write = off;

CREATE TABLE imported_orders_source AS
SELECT
    g AS order_id,
    g % 100000 AS customer_id,
    now() - (g % 365) * interval '1 day' AS order_ts,
    (g % 10000)::numeric / 100 AS amount,
    repeat(md5(g::text), 4) AS payload
FROM generate_series(1, 2000000) AS g;

CREATE TABLE imported_orders_off AS
SELECT *
FROM imported_orders_source;

CREATE TABLE imported_orders_on AS
SELECT *
FROM imported_orders_source;
				
			

Before starting the test, check the skip_intents_writes counter:

				
					\! ./show_skip_intents.sh
				
			

My result:

				
					skip_intents_writes snapshot
YB-TServer       skip_intents_writes
--------------- --------------------
127.0.0.1                       4882
				
			

Enable timing:

				
					\timing on
				
			

Fast Path OFF

Disable the new-relation fast path:

				
					SET yb_enable_new_relation_fastpath_write = off;
				
			

Now add the primary key:

				
					ALTER TABLE imported_orders_off
ADD PRIMARY KEY (order_id);
				
			

My results:

				
					NOTICE:  table rewrite may lead to inconsistencies
DETAIL:  Concurrent DMLs may not be reflected in the new table.
HINT:  See https://github.com/yugabyte/yugabyte-db/issues/19860.
       Set 'ysql_suppress_unsafe_alter_notice' yb-tserver gflag
       to true to suppress this notice.

ALTER TABLE
Time: 80226.823 ms (01:20.227)
				
			

Check the counter again:

				
					\! ./show_skip_intents.sh
				
			
				
					skip_intents_writes snapshot
YB-TServer       skip_intents_writes
--------------- --------------------
127.0.0.1                       4882
				
			

The counter did not change.

Fast Path ON

Now enable the optimization:

				
					SET yb_enable_new_relation_fastpath_write = on;
				
			

Run the same operation against the second copy:

				
					ALTER TABLE imported_orders_on
ADD PRIMARY KEY (order_id);
				
			

Result:

				
					NOTICE:  table rewrite may lead to inconsistencies
DETAIL:  Concurrent DMLs may not be reflected in the new table.
HINT:  See https://github.com/yugabyte/yugabyte-db/issues/19860.
       Set 'ysql_suppress_unsafe_alter_notice' yb-tserver gflag
       to true to suppress this notice.

ALTER TABLE
Time: 52580.399 ms (00:52.580)
				
			

Check the counter one more time:

				
					\! ./show_skip_intents.sh
				
			
				
					skip_intents_writes snapshot
YB-TServer       skip_intents_writes
--------------- --------------------
127.0.0.1                       6835
				
			

This time the counter increased:

				
					4882 β†’ 6835
Delta: +1953
				
			

Primary-Key Rebuild Results

Configuration Elapsed Time skip_intents_writes Delta
❌ Fast path disabled 80.227 sec 0
βœ… Fast path enabled 52.580 sec +1,953
πŸš€ About 34% Less Elapsed Time
Adding the primary key took 80.227 seconds with the fast path disabled and 52.580 seconds with it enabled.
That’s about 27.6 seconds less elapsed time, or a 34.5% reduction, in this test.
The skip_intents_writes counter remained at 4,882 during the fast-path-OFF run, then increased to 6,835 with the fast path enabled β€” a delta of +1,953.
Just like the CREATE TABLE AS demo, we get both pieces of evidence: lower elapsed time and an independent metric confirming that YugabyteDB actually used the optimized write path.
⚠️ About the Table-Rewrite NOTICE
The notice shown during ALTER TABLE ... ADD PRIMARY KEY is associated with the table rewrite itself, not with the new fast-path optimization.
YugabyteDB warns that concurrent DML may not be reflected in the rebuilt table, so this type of operation should be treated as a maintenance activity where concurrent writes are avoided.

Bonus Demo: Fast Path Used β‰  Guaranteed Speedup

The first two demos showed substantial reductions in elapsed time. But that doesn’t mean every operation that uses the fast path will see the same kind of improvement.

A materialized view refresh is a good example.

For this test, the materialized view scans 5 million rows, groups them by customer, and calculates several aggregates. The destination writes are only one part of the total work.

If the sales_orders table from Demo #1 still exists, you can reuse it:

				
					\d sales_orders
				
			
				
					.                      Table "public.sales_orders"
   Column    |           Type           | Collation | Nullable | Default
-------------+--------------------------+-----------+----------+---------
 order_id    | integer                  |           |          |
 customer_id | integer                  |           |          |
 product_id  | integer                  |           |          |
 order_ts    | timestamp with time zone |           |          |
 amount      | numeric                  |           |          |
				
			

If you skipped Demo #1 or already dropped the table, recreate it with the fast path disabled so the setup itself doesn’t affect the skip_intents_writes counter:

				
					SET yb_enable_new_relation_fastpath_write = off;

CREATE TABLE sales_orders AS
SELECT
    g AS order_id,
    g % 100000 AS customer_id,
    g % 1000 AS product_id,
    now() - (g % 365) * interval '1 day' AS order_ts,
    (g % 10000)::numeric / 100 AS amount
FROM generate_series(1, 5000000) AS g;
				
			

Now create two identical materialized views, but leave them unpopulated:

				
					CREATE MATERIALIZED VIEW sales_summary_off AS
SELECT
    customer_id,
    COUNT(*) AS order_count,
    SUM(amount) AS total_amount,
    MAX(order_ts) AS last_order
FROM sales_orders
GROUP BY customer_id
WITH NO DATA;
				
			
				
					CREATE MATERIALIZED VIEW sales_summary_on AS
SELECT
    customer_id,
    COUNT(*) AS order_count,
    SUM(amount) AS total_amount,
    MAX(order_ts) AS last_order
FROM sales_orders
GROUP BY customer_id
WITH NO DATA;
				
			

Before running the refreshes, check the current counter:

				
					\! ./show_skip_intents.sh
				
			
				
					skip_intents_writes snapshot
YB-TServer       skip_intents_writes
--------------- --------------------
127.0.0.1                       6835
				
			

Enable timing:

				
					\timing on
				
			

Fast Path OFF

Disable the optimization:

				
					SET yb_enable_new_relation_fastpath_write = off;
				
			

Refresh the first materialized view:

				
					REFRESH MATERIALIZED VIEW sales_summary_off;
				
			

Result:

				
					REFRESH MATERIALIZED VIEW
Time: 10291.627 ms (00:10.292)
				
			

Check the counter again:

				
					\! ./show_skip_intents.sh
				
			
				
					skip_intents_writes snapshot
YB-TServer       skip_intents_writes
--------------- --------------------
127.0.0.1                       6835
				
			

No change:

				
					6835 β†’ 6835
Delta: 0
				
			

Fast Path ON

Now enable the optimization:

				
					SET yb_enable_new_relation_fastpath_write = on;
				
			

Refresh the second materialized view:

				
					REFRESH MATERIALIZED VIEW sales_summary_on;
				
			

Result:

				
					REFRESH MATERIALIZED VIEW
Time: 9837.404 ms (00:09.837)
				
			

Check the counter one more time:

				
					\! ./show_skip_intents.sh
				
			
				
					skip_intents_writes snapshot
YB-TServer       skip_intents_writes
--------------- --------------------
127.0.0.1                       6868
				
			

This time:

				
					6835 β†’ 6868
Delta: +33
				
			

Materialized View Refresh Results

Configuration Elapsed Time skip_intents_writes Delta
❌ Fast path disabled 10.292 sec 0
βœ… Fast path enabled 9.837 sec +33
πŸ”Ž The Fast Path Was Used… But the Difference Was Small
The materialized view refresh took 10.292 seconds with the fast path disabled and 9.837 seconds with it enabled.
That’s only about 0.45 seconds less elapsed time, or roughly a 4.4% reduction, in this test.
However, the skip_intents_writes counter increased from 6,835 to 6,868 (a delta of +33) confirming that YugabyteDB did use the optimized write path.
In this workload, much of the total runtime is spent scanning 5 million source rows and performing the GROUP BY, COUNT, SUM, and MAX operations. Optimizing the destination writes therefore affects only part of the overall execution time.

This is why the skip_intents_writes metric is so useful. It answers β€œDid YugabyteDB use the fast path?” independently of β€œDid the overall statement become much faster?”

In this case, the answer was yes to the first question, but only slightly to the second.

Important Caveats

This optimization deliberately bypasses part of the normal transactional visibility machinery, so there are some important boundaries.

⚠️ Concurrent Readers Matter
YugabyteDB documents a visibility caveat if another session reads the relation while an optimized load is still in progress. Because rows are written directly to the main store using their individual write timestamps, a concurrent reader can potentially observe only part of the load.
If other sessions may query the relation while it is being populated, disable the optimization for that work:
SET yb_enable_new_relation_fastpath_write = off;

The documentation recommends treating this primarily as a bulk-loading and maintenance optimization, where the relation isn’t being queried concurrently while it is being built.

There is also an internal-retry consideration. Once YugabyteDB has written rows directly through this path, it cannot transparently replay the transaction without risking duplicate writes. A restart/read-retry error that might normally be handled internally can therefore surface to the client.

The optimization also falls back automatically to the normal path in several cases, including colocated tables, temporary tables, transactions after a SAVEPOINT, PL/pgSQL exception blocks, databases with a publication or CDC stream, and REFRESH MATERIALIZED VIEW CONCURRENTLY.

Enabled by Default Doesn’t Mean You Can’t Control It

For standalone qualifying statements in YugabyteDB 2026.1.2+, the feature defaults to:

				
					SELECT setting, boot_val, reset_val FROM pg_settings WHERE name = 'yb_enable_new_relation_fastpath_write';
				
			
				
					.setting | boot_val | reset_val
---------+----------+-----------
 on      | on       | on
(1 row)
				
			

You can disable it at the session level:

				
					SET yb_enable_new_relation_fastpath_write = off;
				
			

or enable it explicitly:

				
					SET yb_enable_new_relation_fastpath_write = on;
				
			

Any user can change the session setting; superuser privileges are not required.

Final Takeaway

The new-relation fast path can make a meaningful difference when YugabyteDB is creating or rebuilding a relation and a significant portion of the work is writing that new data.

In these single-node YugabyteDB 2026.1.2.0 tests, the biggest improvements showed up in CREATE TABLE AS and ALTER TABLE ... ADD PRIMARY KEY, while the materialized-view refresh showed a much smaller gain.

CREATE TABLE AS β€” 5 Million Rows
Fast path OFF
220.228 sec
Fast path ON
138.501 sec
Elapsed time reduction
37.1%
skip_intents_writes delta
+4,882
ADD PRIMARY KEY β€” 2 Million Rows
Fast path OFF
80.227 sec
Fast path ON
52.580 sec
Elapsed time reduction
34.5%
skip_intents_writes delta
+1,953
REFRESH MATERIALIZED VIEW β€” 5 Million Source Rows
Fast path OFF
10.292 sec
Fast path ON
9.837 sec
Elapsed time reduction
4.4%
skip_intents_writes delta
+33

The fast path was clearly used in all three enabled tests, but the overall performance benefit depended on how much of the statement’s runtime was actually spent writing the new relation.

That is what makes the skip_intents_writes metric so useful. It lets you separate two different questions:

  • ● Did YugabyteDB use the optimized write path?
    • Check the skip_intents_writes delta.
  • ● Did that optimization materially improve this workload?

    • Measure the statement’s elapsed time.

πŸ’‘ The Key Point
The new-relation fast path delivered substantial gains when destination writes were a major part of the workload.
In these tests, CREATE TABLE AS and ALTER TABLE ... ADD PRIMARY KEY saw roughly 35–37% less elapsed time, while the more compute-heavy materialized-view refresh saw only a modest improvement even though the fast path was confirmed.
The skip_intents_writes counter helps answer β€œDid YugabyteDB use the optimized write path?”, while the elapsed time tells you whether that optimization materially improved the workload.
Measure both.

That last point is really the takeaway: the fast path can produce significant wins, but the size of the win depends on the workload. The metric tells you whether the optimization was used; the benchmark tells you whether it mattered.

Have Fun!

YugabyteDB is at Postgres Summit US in NYC, Sept. 30–Oct. 2! πŸš€

Stop by our display to meet the team, see live demos, and talk about how distributed PostgreSQL can deliver horizontal scalability, global availability, and ultra-resilience, without giving up PostgreSQL compatibility.

We’re also talking multi-region architectures, HA and failover, scaling for high-concurrency workloads, and building the data foundation for modern AI applications.

Great to be here with the PostgreSQL community! 🐘