09 · Zero-Copy Cloning & Environments¶
CREATE … CLONE makes a full, independent, writable copy of a table, schema or entire database in seconds, regardless of
size, without copying any data. It's one of the features that most changes how teams work: every developer can have a
copy of production to test against, every CI run can get a fresh database, and every risky migration can be rehearsed
on a clone first. This lesson covers how to use it, the rules about what gets cloned, and the case where a clone
quietly stops being free.
Cloning¶
-- a table
CREATE TABLE learn_db.raw.orders_dev CLONE learn_db.raw.orders;
-- a whole database, for a developer
CREATE DATABASE learn_db_dev_ana CLONE learn_db;
-- a database as it was yesterday at 06:00 (Time Travel + clone)
CREATE DATABASE learn_db_audit CLONE learn_db
AT(TIMESTAMP => '2026-10-12 06:00:00'::TIMESTAMP_LTZ);
-- keep the grants of the source table (all except OWNERSHIP)
CREATE TABLE learn_db.raw.orders_copy CLONE learn_db.raw.orders COPY GRANTS;
The clone is immediately usable and fully independent: changes to it don't affect the source, and changes to the
source after the clone don't appear in it. Combining CLONE with AT/BEFORE gives you a point-in-time copy, which
is the standard way to rehearse a recovery (lesson 08) or reproduce yesterday's report exactly.
What comes along, and what doesn't¶
From Snowflake's cloning documentation:
| Object in a cloned database/schema | Behaviour |
|---|---|
| Tables, views, sequences, file formats | cloned |
| Grants on child objects | cloned; grants on the database/schema container itself are not |
| External named stages | cloned (they point at the same bucket) |
| Internal named stages | cloned only with INCLUDE INTERNAL STAGES; table stages are cloned empty |
| Pipes | pipes on external stages are cloned; pipes on internal stages are not |
| Streams | cloned, but unconsumed records in them are inaccessible in the clone |
| Tasks, alerts | cloned suspended |
| Tables with automatic clustering | clustering is suspended on the clone (ALTER TABLE … RESUME RECLUSTER) |
Two of these matter in practice:
- Tasks start suspended, which is what you want. A cloned dev database must not start running production jobs. But it also means a cloned pipeline doesn't "just run": resume what you need deliberately.
- External stages and pipes still point at production storage. A cloned pipe on an external stage, if resumed, loads production's files into your dev tables. Review and repoint these before resuming anything.
A dev/test workflow¶
-- every morning (or per pull request), a fresh copy of production for development
CREATE OR REPLACE DATABASE analytics_dev CLONE analytics_prod;
GRANT USAGE ON DATABASE analytics_dev TO ROLE developer; -- container grants aren't cloned
-- a migration rehearsal
CREATE OR REPLACE DATABASE analytics_migration_test CLONE analytics_prod;
-- run the migration scripts against analytics_migration_test, then run the tests, then:
DROP DATABASE analytics_migration_test;
Pair it with masking policies (Level 3 · 04) so developers on a clone of production don't see personal data they shouldn't. Policies are attached to columns and travel with the clone.
When a clone stops being free¶
A clone shares the source's micro-partitions. Storage is only consumed for micro-partitions that one side rewrites after the clone. Since micro-partitions are immutable, changing even one row in a micro-partition rewrites the whole micro-partition. So the question is how many micro-partitions a change touches, and that depends on how the changed rows are distributed, not on how many there are.
This model (Python, run locally, not a Snowflake measurement) takes a ~1 TB table of 1,000,000,000 rows in 20,000
micro-partitions of 50,000 rows, and computes how much of it a single UPDATE in the clone rewrites:
P, ROWS_PER_P, GB_PER_P = 20_000, 50_000, 0.05
def partitions_touched(rows_updated, scattered):
if scattered: # updated rows spread uniformly at random over the table
return P * (1 - (1 - 1 / P) ** rows_updated)
return min(P, -(-rows_updated // ROWS_PER_P)) # updated rows packed together (e.g. one recent date)
rows updated % of table | clustered: parts GB | scattered: parts GB
10,000 0.001% | 1 0.1 | 7,870 393.5
100,000 0.010% | 2 0.1 | 19,865 993.3
1,000,000 0.100% | 20 1.0 | 20,000 1000.0
10,000,000 1.000% | 200 10.0 | 20,000 1000.0
Updating 0.001% of rows, if they're scattered across the table (say "set is_test = TRUE for these 10,000 customer
IDs"), rewrites about 39% of the table. Updating 0.01% rewrites essentially all of it. The clone now owns close to a
full copy, and so does the source's Time Travel history. The same 10,000 rows packed together (one day's orders, when
the table is ordered by date) touch a single micro-partition.
The same arithmetic governs every UPDATE, DELETE and MERGE in Snowflake, clone or not. It's the cost of the change,
the Time Travel storage it generates, and the work automatic clustering has to redo. Level 2 · 01–02 builds on it:
keeping related rows together in micro-partitions is what makes both queries and changes cheap.
How It Actually Works¶
A table's identity in the metadata is, essentially, a list of micro-partition files. CLONE creates a new metadata
entry whose list points at the same files, which is why it takes the same time for 1 KB and 100 TB. From then on
the two tables evolve independently. A write to either one creates new files referenced only by that table, and the
retired files remain referenced by the other table's current version (and by Time Travel). A file is deleted only when
nothing references it anymore. That includes no table version within its retention, and no clone.
That's also why storage accounting can surprise you. If you drop the source table while a clone still uses its files,
those files don't disappear. They remain, now billed to the table that still needs them. TABLE_STORAGE_METRICS reports
this through its RETAINED_FOR_CLONE_BYTES column.
Cloning a database or schema clones each child object the same way, plus the container metadata. Objects that point outside Snowflake (external stages, pipes on them) are cloned as pointers to the same external resources, which is why they need review.
Common mistakes¶
- Forgetting container grants. The cloned database has none of the source database's grants.
- Resuming cloned pipes or tasks without repointing them. Dev loads production files, or dev jobs write somewhere shared.
- Scattered updates on a clone (anonymizing a column in place, flagging random rows), which turns a free clone into a full copy. Prefer views, or masking policies for anonymization.
- Keeping old clones forever. As the source changes, the clone holds onto partitions the source has retired. Drop clones you no longer need.
- Expecting stream contents to carry over. Unconsumed stream records aren't accessible in the clone.
Exercise¶
- Clone
learn_dbtolearn_db_dev, then compareSHOW GRANTS ON DATABASEfor both, andSHOW GRANTS ON TABLEfor a table in each. - Create a clone of
raw.orders, update 1,000 random rows in it (WHERE UNIFORM(0, 199, RANDOM()) = 0approximates 0.5%), and check the clone'sACTIVE_BYTESinTABLE_STORAGE_METRICSthe next day. With only four small files' worth of data, how many micro-partitions does the table even have? (SYSTEM$CLUSTERING_INFORMATIONreports the total.) - Extend the model: the 10,000 scattered rows are updated every day for 30 days in the source table (not a clone), with 30-day Time Travel. How much Time Travel storage results?
- Write a script (SQL or Snowflake CLI) that recreates a developer database from production every morning and grants it to the developer role, and run it twice. Does the second run disturb anything?