Snowflake SQL Tutorial 98 - CREATE OR REPLACE DATABASE Is Destructive (and UNDROP Can't Save You)

Snowflake SQL · Episode 98

In this post we are going to cover databases in Snowflake: CREATE DATABASE, ALTER DATABASE, DROP DATABASE and UNDROP DATABASE.

A database is the outermost named thing you own. Schemas live inside it, tables live inside those, and every unqualified name you type gets resolved through it. Four statements, and they read like four spellings of the same idea.

They are not. One of them destroys data and reads like a safety measure, and UNDROP cannot get you out of it. We will start by creating a database and looking inside it, and build it up step by step from there.

Every query below was run on a real Snowflake account, and every result you see is the output it gave back, including the refusals.

Episode 98 of the Snowflake SQL: Zero to Hero series on TechBrothersIT.

Let's see where we are standing

There is no sample table to build for this post. Everything we use gets created as we go and dropped again at the end. What we do need is to know where the session is pointing before we move it, so copy this into a worksheet and run it.

Where to run this
-- Nothing to create before this post. Everything on this page is built as we
-- go and dropped again at the end.
-- These two statements are just where we are standing when we start, and the
-- role, because DROP DATABASE and UNDROP DATABASE need one that can do it.
USE ROLE ACCOUNTADMIN;
USE DATABASE DEMO_DB;
USE SCHEMA PUBLIC;

Now let's ask Snowflake the same thing, rather than assume it.

Query
SELECT CURRENT_DATABASE()  AS THE_DATABASE,
       CURRENT_SCHEMA()    AS THE_SCHEMA,
       CURRENT_WAREHOUSE() AS THE_WAREHOUSE,
       CURRENT_ROLE()      AS THE_ROLE;
Result1 row · 4 columns
THE_DATABASETHE_SCHEMATHE_WAREHOUSETHE_ROLE
DEMO_DBPUBLICCOMPUTE_WHACCOUNTADMIN

A database, a schema, a warehouse and a role. Those four functions are the only honest answer to where am I, and we are going to come back to the first two more than once.

And here is what the account already has in it.

Query
SHOW DATABASES;
Result5 rows · 14 columns · scroll →
created_onnameis_defaultis_currentoriginownercommentoptionsretention_timekindowner_role_typeobject_visibilitydata_quality_monitoring_settingsresharing_settings
2026-09-21 19:10:46.265000-07:00DEMO_DBNYACCOUNTADMIN1STANDARDROLENULLNULLNULL
2026-09-21 18:33:42.814000-07:00SNOWFLAKENNSNOWFLAKE.ACCOUNT_USAGE0APPLICATIONNULLNULLNULL
2026-09-21 18:33:47.866000-07:00SNOWFLAKE_LEARNING_DBNNACCOUNTADMINCreated by Snowflake during account provisioning1STANDARDROLENULLNULLNULL
2026-09-21 18:33:47.725000-07:00SNOWFLAKE_SAMPLE_DATANNSFSALESSHARED.SFC_SAMPLES_AWSUSEAST2.SAMPLE_DATAACCOUNTADMINPreloaded TPCH Data provided by Snowflake during account provisioning0IMPORTED DATABASEROLENULLNULLNULL
2026-09-21 18:37:01.893000-07:00USER$TECHBROTHERSITAAMIRNN1PERSONAL DATABASENULLNULLNULL

Five databases and fourteen columns describing them. DEMO_DB is ours and the rest arrived with the account. Have a look at the IS_CURRENT column: Y on one row and N on the others, which is the same fact CURRENT_DATABASE() just gave us.

Empty is not NULL

The COMMENT cell on DEMO_DB is marked empty rather than NULL, and that is not the grid being fussy. SHOW output returns an empty string where there is nothing to say, while the INFORMATION_SCHEMA views we use later return a real NULL. The grids on this page draw those two differently, so you can see which one you are dealing with.

Let's create a database and look inside it

One statement, with a comment on it so we have something to watch later.

Query
CREATE OR REPLACE DATABASE DDL_DEMO_T98
  COMMENT = 'episode 98 probe - dropped at the end of this file';
Result1 row · 1 column
status
Database DDL_DEMO_T98 successfully created.
Yes, that is OR REPLACE

Read that statement again. It says CREATE OR REPLACE, in an episode about how dangerous those two words are. The demo file has to be re-runnable, and on a throwaway name that nothing else owns it is harmless. By the end of this post you will know exactly why it would not be harmless anywhere else.

So what does a brand new, empty database already contain?

Query
SHOW SCHEMAS IN DATABASE DDL_DEMO_T98;
Result2 rows · 15 columns · scroll →
created_onnameis_defaultis_currentdatabase_nameownercommentoptionsretention_timeowner_role_typeclassification_profile_databaseclassification_profile_schemaclassification_profileobject_visibilityis_nested
2026-09-28 13:00:46.321000-07:00INFORMATION_SCHEMANNDDL_DEMO_T98Views describing the contents of schemas in this database1NULLNULLNULLNULLfalse
2026-09-28 13:00:46.032000-07:00PUBLICNYDDL_DEMO_T98ACCOUNTADMIN1ROLENULLNULLNULLNULLfalse

Two schemas, and we asked for neither of them. INFORMATION_SCHEMA is the catalog, and PUBLIC is a working schema Snowflake makes for you. Remember that PUBLIC is a default rather than a law, because that is where Episode 99 starts.

Let's count the same thing from the catalog instead of reading it off a SHOW.

Query
SELECT COUNT(*)                                  AS SCHEMAS_PRESENT,
       ARRAY_AGG(SCHEMA_NAME)                    AS THEIR_NAMES
FROM DDL_DEMO_T98.INFORMATION_SCHEMA.SCHEMATA;
Result1 row · 2 columns
SCHEMAS_PRESENTTHEIR_NAMES
2[ "PUBLIC", "INFORMATION_SCHEMA" ]

SCHEMAS_PRESENT is 2 and THEIR_NAMES lists both of them. A number is easier to assert on than a grid you have to look at.

And here is what the catalog recorded about the database itself.

Query
SELECT DATABASE_NAME, COMMENT, RETENTION_TIME, IS_TRANSIENT
FROM DDL_DEMO_T98.INFORMATION_SCHEMA.DATABASES
WHERE DATABASE_NAME = 'DDL_DEMO_T98';
Result1 row · 4 columns
DATABASE_NAMECOMMENTRETENTION_TIMEIS_TRANSIENT
DDL_DEMO_T98episode 98 probe - dropped at the end of this file1NO

Our comment is there, RETENTION_TIME is 1, and IS_TRANSIENT is NO. That retention value is the number of days Time Travel will hold on to this database after it is dropped, and it becomes the most important column on this page later on.

Now let's point the session at it

USE DATABASE moves the session. One statement, no result worth reading.

Query
USE DATABASE DDL_DEMO_T98;
Result1 row · 1 column
status
Statement executed successfully.

Here is where I was wrong, and it is going in the post rather than being quietly deleted. I expected CURRENT_SCHEMA() to come back NULL after a bare USE DATABASE, because I had not named a schema.

Query
SELECT CURRENT_DATABASE() AS THE_DATABASE,
       CURRENT_SCHEMA()   AS THE_SCHEMA;
Result1 row · 2 columns
THE_DATABASETHE_SCHEMA
DDL_DEMO_T98PUBLIC

It is PUBLIC. USE DATABASE picked a schema for me. So the next statement, which I had marked in the demo file as an expected failure, creates a table without naming a schema at all.

The query I expected to fail
CREATE TABLE ORPHAN_T98 (ID INT);
Result1 row · 1 column
status
Table ORPHAN_T98 successfully created.

Table ORPHAN_T98 successfully created. It worked. The prediction was wrong, the capture says so, and that is one fewer thing for you to trip over.

Let's build something worth losing

Relying on USE DATABASE to pick a schema is fine in a worksheet and not fine in a file, so let's name the schema anyway.

Query
USE SCHEMA PUBLIC;
Result1 row · 1 column
status
Statement executed successfully.

Now a table, with a row in it whose only job is to tell one database from another later on.

Query
CREATE OR REPLACE TABLE KEEPSAKE_T98 (ID INT, NOTE VARCHAR);
Result1 row · 1 column
status
Table KEEPSAKE_T98 successfully created.
Query
INSERT INTO KEEPSAKE_T98 (ID, NOTE)
SELECT 1, 'this row is how we will tell the databases apart';
Result1 row · 1 column
number of rows inserted
1
Query
SELECT ID, NOTE FROM KEEPSAKE_T98 ORDER BY ID;
Result1 row · 2 columns
IDNOTE
1this row is how we will tell the databases apart

That is the row to keep an eye on. From here on, whenever a grid asks whether the row is still there, it is asking about this one.

Now let's rename the database

ALTER DATABASE ... RENAME TO gives the same database a new name.

Query
ALTER DATABASE DDL_DEMO_T98 RENAME TO DDL_DEMO_RENAMED_T98;
Result1 row · 1 column
status
Statement executed successfully.

Let's ask two things at once: where is the session, and is the row still there under the new name?

Query
SELECT CURRENT_DATABASE()                                  AS THE_DATABASE,
       (SELECT COUNT(*) FROM DDL_DEMO_RENAMED_T98.PUBLIC.KEEPSAKE_T98)
         AS ROWS_STILL_THERE;
Result1 row · 2 columns
THE_DATABASEROWS_STILL_THERE
NULL1

ROWS_STILL_THERE is 1. The data is fine. Now look at the first column. THE_DATABASE is NULL.

Not the new name. Let's ask that more carefully, because NULL is a strange answer to where am I.

Query
SELECT CURRENT_DATABASE()                    AS THE_DATABASE,
       CURRENT_DATABASE() = 'DDL_DEMO_RENAMED_T98' AS IT_FOLLOWED,
       CURRENT_DATABASE() = 'DDL_DEMO_T98'   AS IT_DID_NOT;
Result1 row · 3 columns
THE_DATABASEIT_FOLLOWEDIT_DID_NOT
NULLNULLNULL

IT_FOLLOWED is NULL and IT_DID_NOT is NULL. Not TRUE, not FALSE. Both comparisons are NULL because CURRENT_DATABASE() is itself NULL, and NULL = anything is NULL. That is the rule from Episode 94, one level up, turning up in the last place you would look for it.

The session got orphaned, quietly

The rename did not carry the session with it, and it did not leave it on the old name either. It left it pointing at nothing, and nothing told us. The next unqualified query you run after that fails for a reason that looks completely unrelated to the rename you just did.

Let's put the name back before we do anything else.

Query
ALTER DATABASE DDL_DEMO_RENAMED_T98 RENAME TO DDL_DEMO_T98;
Result1 row · 1 column
status
Statement executed successfully.

Let's set a comment and a retention time

ALTER DATABASE ... SET changes properties rather than the name. Let's change the comment and set the retention explicitly.

Query
ALTER DATABASE DDL_DEMO_T98 SET
  COMMENT = 'comment changed by ALTER'
  DATA_RETENTION_TIME_IN_DAYS = 1;
Result1 row · 1 column
status
Statement executed successfully.
Query
SELECT DATABASE_NAME, COMMENT, RETENTION_TIME
FROM DDL_DEMO_T98.INFORMATION_SCHEMA.DATABASES
WHERE DATABASE_NAME = 'DDL_DEMO_T98';
Result1 row · 3 columns
DATABASE_NAMECOMMENTRETENTION_TIME
DDL_DEMO_T98comment changed by ALTER1

The comment now reads comment changed by ALTER and RETENTION_TIME is 1. Both moved, so ALTER DATABASE does exactly what you would expect to the database.

It is the session it is careless with. Hold on to that comment, by the way. It is how we are going to identify this database after it stops existing.

Now let's try three ways to create one that already exists

We have a database with a table in it, a row in the table and a comment on the database. Now let's run CREATE DATABASE at it three different ways and watch what each one does.

One: IF NOT EXISTS

Query
CREATE DATABASE IF NOT EXISTS DDL_DEMO_T98;
Result1 row · 1 column
status
DDL_DEMO_T98 already exists, statement succeeded.

DDL_DEMO_T98 already exists, statement succeeded. It ran and it did nothing. Let's check the row.

Query
SELECT COUNT(*) AS ROWS_STILL_THERE FROM DDL_DEMO_T98.PUBLIC.KEEPSAKE_T98;
Result1 row · 1 column
ROWS_STILL_THERE
1

ROWS_STILL_THERE is 1. Nothing happened to anything. That is the safe one.

Two: plain CREATE DATABASE

Query
CREATE DATABASE DDL_DEMO_T98;
Query failed — Snowflake returned an error
002002 (42710): SQL compilation error:
Object 'DDL_DEMO_T98' already exists.

A clean refusal, and it names the object. This one is safe as well, for the simplest possible reason: it stopped.

Three: CREATE OR REPLACE

Query
CREATE OR REPLACE DATABASE DDL_DEMO_T98;
Result1 row · 1 column
status
Database DDL_DEMO_T98 successfully created.

Database DDL_DEMO_T98 successfully created. It ran. No warning, no confirmation, nothing asking whether we were sure.

So what happened to what was in there?

Let's ask for the table we built.

Query
SELECT COUNT(*) AS ROWS_STILL_THERE FROM DDL_DEMO_T98.PUBLIC.KEEPSAKE_T98;
Query failed — Snowflake returned an error
002003 (42S02): SQL compilation error:
Object 'DDL_DEMO_T98.PUBLIC.KEEPSAKE_T98' does not exist or not authorized.

does not exist or not authorized. The table is gone, and so is the row in it.

Let's count what is in the schema now.

Query
SELECT COUNT(*) AS TABLES_IN_PUBLIC
FROM DDL_DEMO_T98.INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA = 'PUBLIC';
Result1 row · 1 column
TABLES_IN_PUBLIC
0

TABLES_IN_PUBLIC is 0. And the comment we set with ALTER?

Query
SELECT DATABASE_NAME, COMMENT, COMMENT IS NULL AS COMMENT_IS_GONE
FROM DDL_DEMO_T98.INFORMATION_SCHEMA.DATABASES
WHERE DATABASE_NAME = 'DDL_DEMO_T98';
Result1 row · 3 columns
DATABASE_NAMECOMMENTCOMMENT_IS_GONE
DDL_DEMO_T98NULLTRUE

COMMENT is NULL and COMMENT_IS_GONE is TRUE. So CREATE OR REPLACE DATABASE dropped our database and built a new empty one on the same name. The tables, the comment and the retention setting went with it.

Read the three statements again, as a reader would.

What you typedWhat it did
CREATE DATABASE IF NOT EXISTSNothing. The row was still there afterwards.
CREATE DATABASERefused, and said the object already exists.
CREATE OR REPLACE DATABASEDropped it and built a new empty one. No confirmation.
Which word to type

The one that sounds most like make sure this exists is the destructive one. Use IF NOT EXISTS when that is what you mean. Use plain CREATE when you want to be told it is already there. Keep OR REPLACE for things you are genuinely rebuilding, and say so out loud when you write it.

Can UNDROP save us?

UNDROP DATABASE exists, and the replace dropped a database. So this is recoverable, surely.

Query
UNDROP DATABASE DDL_DEMO_T98;
Query failed — Snowflake returned an error
002002 (42710): SQL compilation error:
Object 'DDL_DEMO_T98' already exists.

Refused, with the same error we got from the plain CREATE a moment ago: Object 'DDL_DEMO_T98' already exists.

The old database is not gone. It is in Time Travel, with our comment on it, and we will see it listed later in this post. But UNDROP works by resolving a name, and the name is taken. The new empty database is sitting on it.

So the recovery is to rename or drop the replacement first, and then UNDROP. That is two statements you have to already know, and you will be looking them up at the worst possible moment.

The actual lesson

The lesson here is not UNDROP. UNDROP is excellent and we are about to watch it work perfectly. The lesson is not to type OR REPLACE unless you mean it, because the statement that would normally rescue you is the one that this particular mistake disables.

Now let's do it properly: drop, then undrop

Let's put a table back so we have something to lose on purpose this time. First the session, which the replace left pointing at a database that no longer exists.

Point the session at the new database
USE DATABASE DDL_DEMO_T98;
Result1 row · 1 column
status
Statement executed successfully.
Query
USE SCHEMA PUBLIC;
Result1 row · 1 column
status
Statement executed successfully.
Query
CREATE OR REPLACE TABLE KEEPSAKE_T98 (ID INT, NOTE VARCHAR);
Result1 row · 1 column
status
Table KEEPSAKE_T98 successfully created.
Query
INSERT INTO KEEPSAKE_T98 (ID, NOTE) SELECT 2, 'the row that survives UNDROP';
Result1 row · 1 column
number of rows inserted
1

Now drop the whole database, which is the one we are standing in.

Query
DROP DATABASE DDL_DEMO_T98;
Result1 row · 1 column
status
DDL_DEMO_T98 successfully dropped.

DDL_DEMO_T98 successfully dropped. It let us drop the database the session was pointing at, with no warning at all. So where is the session now?

Query
SELECT CURRENT_DATABASE()               AS THE_DATABASE,
       CURRENT_DATABASE() IS NULL       AS IT_IS_NULL_NOW,
       CURRENT_SCHEMA()                 AS THE_SCHEMA;
Result1 row · 3 columns
THE_DATABASEIT_IS_NULL_NOWTHE_SCHEMA
NULLTRUENULL

IT_IS_NULL_NOW is TRUE, and THE_SCHEMA is NULL as well. Orphaned, exactly as it was after the rename, and just as silently.

And here is the one we lost, still in Time Travel

Now let's look up that name in the drop history.

Query
SHOW DATABASES HISTORY LIKE 'DDL_DEMO_T98';
Result2 rows · 15 columns · scroll →
created_onnameis_defaultis_currentoriginownercommentoptionsretention_timedropped_onkindowner_role_typeobject_visibilitydata_quality_monitoring_settingsresharing_settings
2026-09-28 13:00:45.966000-07:00DDL_DEMO_T98NNACCOUNTADMINcomment changed by ALTER12026-09-28 13:00:52.482000-07:00STANDARDROLENULLNULLNULL
2026-09-28 13:00:52.404000-07:00DDL_DEMO_T98NNACCOUNTADMIN12026-09-28 13:00:55.428000-07:00STANDARDROLENULLNULLNULL

Two rows, both called DDL_DEMO_T98, and both with a DROPPED_ON timestamp. The one with the earlier CREATED_ON still carries comment changed by ALTER. That is ours. That is the database our table and our first row were in, and it was dropped by the OR REPLACE rather than by us.

So it really is right there, visible, in a grid, with our own comment on it to prove which one it is. And when the name was taken, UNDROP could not reach it.

Let's run the UNDROP now that the name is free.

Query
UNDROP DATABASE DDL_DEMO_T98;
Result1 row · 1 column
status
Database DDL_DEMO_T98 successfully restored.

Database DDL_DEMO_T98 successfully restored. And the row?

Query
SELECT ID, NOTE FROM DDL_DEMO_T98.PUBLIC.KEEPSAKE_T98 ORDER BY ID;
Result1 row · 2 columns
IDNOTE
2the row that survives UNDROP

the row that survives UNDROP came back with the database. Note which row this is: it is the one we put in just before dropping it ourselves, not the one the replace took away. UNDROP restores the most recently dropped database of that name, which is the replacement.

The whole condition, in one line

UNDROP is genuinely good, and the condition is simple: the name has to be free. Drop something, undrop it, and your data comes back. Replace something, and the name is occupied by the thing that replaced it.

One last thing: undropping something that was never dropped

Let's ask for a database that has never existed, because the error message is worth reading twice.

Query
UNDROP DATABASE DDL_DEMO_NEVER_EXISTED_T98;
Query failed — Snowflake returned an error
090025 (02000): Database DDL_DEMO_NEVER_EXISTED_T98 did not exist or was purged.

did not exist or was purged. Those are two very different situations behind one message, and the second one is the warning. Time Travel is not forever. Past the retention window there is nothing left to undrop, and you get the same sentence you would get for a typo.

That is why the RETENTION_TIME column from the second section matters. It is the size of the window you have.

Let's clean up

Query
DROP DATABASE IF EXISTS DDL_DEMO_T98;
Result1 row · 1 column
status
DDL_DEMO_T98 successfully dropped.
Query
USE DATABASE DEMO_DB;
Result1 row · 1 column
status
Statement executed successfully.
Query
USE SCHEMA PUBLIC;
Result1 row · 1 column
status
Statement executed successfully.

And a check that we are back where we started, reading a table in DEMO_DB through an unqualified name.

Query
SELECT CURRENT_DATABASE() AS THE_DATABASE,
       CURRENT_SCHEMA()   AS THE_SCHEMA,
       (SELECT COUNT(*) FROM EVENTS_JSON) AS DEMO_DB_IS_READABLE;
Result1 row · 3 columns
THE_DATABASETHE_SCHEMADEMO_DB_IS_READABLE
DEMO_DBPUBLIC4

DEMO_DB, PUBLIC, and four rows read out of EVENTS_JSON without qualifying it. The session is pointing somewhere real again.

Key points

  • A brand new database is not empty. It arrives with PUBLIC and INFORMATION_SCHEMA.
  • USE DATABASE selects PUBLIC for you. I predicted otherwise and the capture proved me wrong.
  • ALTER DATABASE does what you expect to the database: the rename, the comment and the retention all take effect.
  • A rename leaves the session pointing at nothing. Not the new name, not the old name. CURRENT_DATABASE() comes back NULL, and nothing tells you.
  • The comparisons come back NULL rather than FALSE, because NULL = anything is NULL.
  • CREATE DATABASE IF NOT EXISTS does nothing to what is there. Plain CREATE DATABASE refuses and says so.
  • CREATE OR REPLACE DATABASE drops it and builds a new empty one, with no warning and no confirmation. The tables, the comment and the retention setting go with it.
  • UNDROP is refused while the name is taken, with the same already exists error a plain CREATE gives.
  • SHOW DATABASES HISTORY shows you the one you lost. You can see it and you cannot name it.
  • The recovery is to rename or drop the replacement first, then UNDROP. Learn that now rather than then.
  • DROP then UNDROP works perfectly when the name is free, and the rows come back with the database.
  • Dropping the database you are standing in orphans the session the same way a rename does.
  • did not exist or was purged is the message that tells you Time Travel is not forever.
Next in this series

Episode 99 — schemas. Two schemas, one table name, and the identical query returning different rows depending on a USE statement you typed some minutes earlier. Plus the question I did not know the answer to: can you drop the PUBLIC schema?

Every query and every result grid on this page was captured from a live Snowflake account (version 10.34.101) running the demo script for this episode, including the four refusals. The databases, the schema and the table are throwaway objects created and dropped by that script. Where the spoken script and the capture disagreed about when a statement ran, this post follows the capture. Nothing is timed and no performance claims are made anywhere in this series. Statement behaviour follows the Snowflake CREATE DATABASE documentation.

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.