Instant database cloning in YugabyteDB is one of those features that feels almost magical the first time you use it.
You can create a writable clone of a database in seconds, either as of now or as of a previous point in time, without copying all of the underlying table data.
Instead of duplicating every row up front, the clone initially shares the same data files as the source database. Once the clone exists, changes made to either database are tracked separately, keeping the original and the clone logically independent.
That makes cloning extremely useful for:
- ● recovering accidentally deleted or corrupted data
- ● creating realistic development and test environments
- ● validating application changes against production-like data
- ● forensic analysis after a user or application mistake
But there is an important detail hiding behind the scenes:
- The data clone may be zero-copy, but the YSQL schema still has work to do.
In this tip, we’ll look at why clone time can still increase as the number of schema objects grows.
For a general overview of YugabyteDB instant database cloning and why it is useful for zero-copy point-in-time recovery, see the earlier YugabyteDB Tips post:
What Instant Database Cloning Does
At a high level, instant database cloning lets you create a new database from an existing one:
CREATE DATABASE clone_db TEMPLATE original_db;
You can also clone the database as of a specific point in time:
CREATE DATABASE clone_db TEMPLATE original_db AS OF '2026-05-28 10:00:00';
or by using a Unix timestamp in microseconds:
CREATE DATABASE clone_db TEMPLATE original_db AS OF 1723146703674480;
The key point is that the clone does not start by physically copying every row from every table.
Instead, the clone initially shares the same underlying data files with the source database. That is why cloning can be fast and space efficient.
The Part That Is Easy to Miss
Even though the table data is not fully copied up front, the database schema still has to exist in the cloned database.
Behind the scenes, the YSQL clone path uses dump/restore-style behavior for the schema.
That means the clone process has to recreate schema objects such as:
- ● tables
- ● indexes
- ● constraints
- ● sequences
- ● views
- ● extensions
- ● other database-level metadata
This is where clone time can become tied to the number of objects in the database.
A database with one table and one index is very different from a database with one table and 99 indexes, or hundreds of tables, indexes, constraints, and other schema objects.
Instant database cloning is zero-copy for the underlying data files, but it is not zero-work for the YSQL schema. The more schema objects the source database has, the more DDL work the clone process may need to perform.
Demo Setup
For this test, I created several databases where the number in the database name matches the total number of objects.
For example:
CREATE DATABASE db_2_objects;
CREATE DATABASE db_10_objects;
CREATE DATABASE db_50_objects;
CREATE DATABASE db_100_objects;
The pattern is simple:
- ●
db_2_objectshas 1 table and 1 index. - ●
db_10_objectshas 1 table and 9 indexes. - ●
db_50_objectshas 1 table and 49 indexes. - ●
db_100_objectshas 1 table and 99 indexes.
Here are the test databases:
yugabyte=# \list db*
List of databases
Name | Owner | Encoding | Collate | Ctype | ICU Locale | Locale Provider | Access privileges
----------------+----------+----------+---------+-------------+------------+-----------------+-------------------
db_100_objects | yugabyte | UTF8 | C | en_US.UTF-8 | | libc |
db_50_objects | yugabyte | UTF8 | C | en_US.UTF-8 | | libc |
db_10_objects | yugabyte | UTF8 | C | en_US.UTF-8 | | libc |
db_2_objects | yugabyte | UTF8 | C | en_US.UTF-8 | | libc |
In db_10_objects, the schema looks like this:
db_10_objects=# \d
List of relations
Schema | Name | Type | Owner
--------+-------------+-------+----------
public | object_test | table | yugabyte
(1 row)
And the table has 9 indexes:
db_10_objects=# \d object_test
Table "public.object_test"
Column | Type | Collation | Nullable | Default
--------+---------+-----------+----------+---------
id | integer | | |
c_0001 | integer | | |
c_0002 | integer | | |
c_0003 | integer | | |
c_0004 | integer | | |
c_0005 | integer | | |
c_0006 | integer | | |
c_0007 | integer | | |
c_0008 | integer | | |
c_0009 | integer | | |
Indexes:
"idx_object_test_0001" lsm (c_0001 HASH)
"idx_object_test_0002" lsm (c_0002 HASH)
"idx_object_test_0003" lsm (c_0003 HASH)
"idx_object_test_0004" lsm (c_0004 HASH)
"idx_object_test_0005" lsm (c_0005 HASH)
"idx_object_test_0006" lsm (c_0006 HASH)
"idx_object_test_0007" lsm (c_0007 HASH)
"idx_object_test_0008" lsm (c_0008 HASH)
"idx_object_test_0009" lsm (c_0009 HASH)
Clone Timing Test
Next, I enabled timing in ysqlsh and cloned each database:
yugabyte=# \timing
Timing is on.
yugabyte=# CREATE DATABASE clone_db_2_objects TEMPLATE db_2_objects;
CREATE DATABASE
Time: 8839.047 ms (00:08.839)
yugabyte=# CREATE DATABASE clone_db_10_objects TEMPLATE db_10_objects;
CREATE DATABASE
Time: 8972.910 ms (00:08.973)
yugabyte=# CREATE DATABASE clone_db_50_objects TEMPLATE db_50_objects;
CREATE DATABASE
Time: 17296.428 ms (00:17.296)
yugabyte=# CREATE DATABASE clone_db_100_objects TEMPLATE db_100_objects;
CREATE DATABASE
Time: 25723.646 ms (00:25.724)
Here is the result:
| Source Database | Objects | Clone Time |
| db_2_objects | 1 table + 1 index | 8.839 seconds |
| db_10_objects | 1 table + 9 indexes | 8.973 seconds |
| db_50_objects | 1 table + 49 indexes | 17.296 seconds |
| db_100_objects | 1 table + 99 indexes | 25.724 seconds |
These tests were run on a low-powered single-node VM.
On a larger or more powerful environment, the absolute clone times will likely be lower. The key observation is the pattern: clone time increased as the number of schema objects increased.
The important part is not the exact number of seconds. That will vary by cluster size, hardware, load, YugabyteDB version, catalog state, and other factors.
The important part is the pattern:
- As the number of schema objects increased, the clone took longer.
The smaller databases cloned in about 9 seconds. The 50-object database took about 17 seconds. The 100-object database took about 26 seconds.
That is a strong clue that object count matters.
The clone time did not increase because the table data was being copied row by row. It increased because the clone had more YSQL schema objects to recreate.
What Was the Clone Doing?
Using pg_stat_statements, we can see some of the DDL executed inside the cloned database.
For clone_db_10_objects, the clone created the table and then created each index:
SELECT query, mean_exec_time
FROM pg_stat_statements
JOIN pg_database ON oid = dbid
WHERE datname = 'clone_db_10_objects'
AND query ILIKE 'CREATE%'
ORDER BY query DESC;
Example output:
. query | mean_exec_time
----------------------------------------------------------------------------------------------------------------------+----------------
CREATE TABLE public.object_test ( +| 90.571691
id integer, +|
c_0001 integer, +|
c_0002 integer, +|
c_0003 integer, +|
c_0004 integer, +|
c_0005 integer, +|
c_0006 integer, +|
c_0007 integer, +|
c_0008 integer, +|
c_0009 integer +|
) +|
SPLIT INTO 1 TABLETS |
CREATE INDEX NONCONCURRENTLY idx_object_test_0009 ON public.object_test USING lsm (c_0009 HASH) SPLIT INTO 1 TABLETS | 56.223371
CREATE INDEX NONCONCURRENTLY idx_object_test_0008 ON public.object_test USING lsm (c_0008 HASH) SPLIT INTO 1 TABLETS | 41.78858
CREATE INDEX NONCONCURRENTLY idx_object_test_0007 ON public.object_test USING lsm (c_0007 HASH) SPLIT INTO 1 TABLETS | 86.113798
CREATE INDEX NONCONCURRENTLY idx_object_test_0006 ON public.object_test USING lsm (c_0006 HASH) SPLIT INTO 1 TABLETS | 44.783232
CREATE INDEX NONCONCURRENTLY idx_object_test_0005 ON public.object_test USING lsm (c_0005 HASH) SPLIT INTO 1 TABLETS | 54.377821
CREATE INDEX NONCONCURRENTLY idx_object_test_0004 ON public.object_test USING lsm (c_0004 HASH) SPLIT INTO 1 TABLETS | 65.602368
CREATE INDEX NONCONCURRENTLY idx_object_test_0003 ON public.object_test USING lsm (c_0003 HASH) SPLIT INTO 1 TABLETS | 47.331031
CREATE INDEX NONCONCURRENTLY idx_object_test_0002 ON public.object_test USING lsm (c_0002 HASH) SPLIT INTO 1 TABLETS | 79.318478
CREATE INDEX NONCONCURRENTLY idx_object_test_0001 ON public.object_test USING lsm (c_0001 HASH) SPLIT INTO 1 TABLETS | 98.812748
(10 rows)
The output shows that the clone operation executed DDL inside the cloned database.
For clone_db_10_objects, that included:
- ● 1 CREATE TABLE statement
- ● 9 CREATE INDEX NONCONCURRENTLY statements
Each index was recreated with CREATE INDEX NONCONCURRENTLY, and each object included SPLIT INTO 1 TABLETS.
That is the important clue.
The clone is not simply flipping a single metadata pointer and calling it done. For YSQL databases, schema objects are being recreated in the cloned database. The table data is zero-copy, but the schema still has to be rebuilt.
The clone is fast because it avoids copying all table data up front.
But the schema still has to be reconstructed. In this example, pg_stat_statements shows one CREATE TABLE statement and nine CREATE INDEX NONCONCURRENTLY statements executed inside the cloned database.
Why Are Indexes Created NONCONCURRENTLY?
Notice that the indexes were created using:
CREATE INDEX NONCONCURRENTLY
That is a good thing in this context.
YugabyteDB supports both concurrent and nonconcurrent index creation. Concurrent index creation is designed for online workloads where the table may still be receiving writes.
For a clone restore-style operation, there should not be user writes happening inside the cloned database while the clone is still being built. So using NONCONCURRENTLY makes sense because the system is rebuilding schema objects as part of a controlled clone operation.
For more on why CREATE INDEX NONCONCURRENTLY can be faster in controlled situations where there are no concurrent writes, see:
Faster Index Creation
Why Object Count Matters
Think of instant database cloning as two layers of work:
| Layer | What Happens | Performance Impact |
| Data layer | The clone initially shares existing data files with the source database. | Fast and space efficient. |
| Schema layer | YSQL schema objects are recreated in the cloned database. | Can grow with object count. |
So if a database has:
- ● 10 tables
- ● 200 indexes
- ● 50 sequences
- ● 100 constraints
- ● many partitions
- ● extensions
- ● views
- ● materialized views
then the clone has more schema work to do than a database with only a few objects.
The clone may still be dramatically faster than a full logical export/import or physical copy, but it is not independent of schema complexity.
Practical Takeaways
Here is how I would think about clone performance:
- ● Large data volume alone does not necessarily mean slow clone creation.
- ● Many schema objects can increase clone time.
- ● Index-heavy schemas can make clone time more noticeable.
- ●
pg_stat_statementscan help reveal the DDL being executed during clone. - ●
yb_database_clones()can be used to check clone state after creation. - ● Clones are excellent for recovery, testing, and development, but they still create additional logical tablets and metadata.
Final Takeaway
Instant database cloning in YugabyteDB is powerful because it avoids copying all of the table data up front.
That is the “instant” part.
But the YSQL schema still has to be recreated in the cloned database.
So when you test clone performance, do not only think about database size in GB or TB. Also think about object count.
A 4 TB database with a simple schema may clone very quickly.
A much smaller database with thousands of tables, indexes, constraints, partitions, and sequences may take longer than expected because the clone has more schema work to perform.
Have Fun!
Our future son-in-law has been driving an older, economical car since we met him a few years ago.
But today, he’s the proud owner of a brand-new 2027 Toyota Land Cruiser, complete with the heritage grille and Trail Dust color.
Pretty awesome upgrade! 🚙
