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
- Let's create a database and look inside it
- Now let's point the session at it
- Let's build something worth losing
- Now let's rename the database
- Let's set a comment and a retention time
- Now let's try three ways to create one that already exists
- So what happened to what was in there?
- Can UNDROP save us?
- Now let's do it properly: drop, then undrop
- And here is the one we lost, still in Time Travel
- One last thing: undropping something that was never dropped
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.
-- 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.
SELECT CURRENT_DATABASE() AS THE_DATABASE,
CURRENT_SCHEMA() AS THE_SCHEMA,
CURRENT_WAREHOUSE() AS THE_WAREHOUSE,
CURRENT_ROLE() AS THE_ROLE;| THE_DATABASE | THE_SCHEMA | THE_WAREHOUSE | THE_ROLE |
|---|---|---|---|
| DEMO_DB | PUBLIC | COMPUTE_WH | ACCOUNTADMIN |
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.
SHOW DATABASES;| created_on | name | is_default | is_current | origin | owner | comment | options | retention_time | kind | owner_role_type | object_visibility | data_quality_monitoring_settings | resharing_settings |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 2026-09-21 19:10:46.265000-07:00 | DEMO_DB | N | Y | ACCOUNTADMIN | 1 | STANDARD | ROLE | NULL | NULL | NULL | |||
| 2026-09-21 18:33:42.814000-07:00 | SNOWFLAKE | N | N | SNOWFLAKE.ACCOUNT_USAGE | 0 | APPLICATION | NULL | NULL | NULL | ||||
| 2026-09-21 18:33:47.866000-07:00 | SNOWFLAKE_LEARNING_DB | N | N | ACCOUNTADMIN | Created by Snowflake during account provisioning | 1 | STANDARD | ROLE | NULL | NULL | NULL | ||
| 2026-09-21 18:33:47.725000-07:00 | SNOWFLAKE_SAMPLE_DATA | N | N | SFSALESSHARED.SFC_SAMPLES_AWSUSEAST2.SAMPLE_DATA | ACCOUNTADMIN | Preloaded TPCH Data provided by Snowflake during account provisioning | 0 | IMPORTED DATABASE | ROLE | NULL | NULL | NULL | |
| 2026-09-21 18:37:01.893000-07:00 | USER$TECHBROTHERSITAAMIR | N | N | 1 | PERSONAL DATABASE | NULL | NULL | NULL |
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.
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.
CREATE OR REPLACE DATABASE DDL_DEMO_T98
COMMENT = 'episode 98 probe - dropped at the end of this file';| status |
|---|
| Database DDL_DEMO_T98 successfully created. |
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?
SHOW SCHEMAS IN DATABASE DDL_DEMO_T98;| created_on | name | is_default | is_current | database_name | owner | comment | options | retention_time | owner_role_type | classification_profile_database | classification_profile_schema | classification_profile | object_visibility | is_nested |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 2026-09-28 13:00:46.321000-07:00 | INFORMATION_SCHEMA | N | N | DDL_DEMO_T98 | Views describing the contents of schemas in this database | 1 | NULL | NULL | NULL | NULL | false | |||
| 2026-09-28 13:00:46.032000-07:00 | PUBLIC | N | Y | DDL_DEMO_T98 | ACCOUNTADMIN | 1 | ROLE | NULL | NULL | NULL | NULL | false |
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.
SELECT COUNT(*) AS SCHEMAS_PRESENT,
ARRAY_AGG(SCHEMA_NAME) AS THEIR_NAMES
FROM DDL_DEMO_T98.INFORMATION_SCHEMA.SCHEMATA;| SCHEMAS_PRESENT | THEIR_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.
SELECT DATABASE_NAME, COMMENT, RETENTION_TIME, IS_TRANSIENT
FROM DDL_DEMO_T98.INFORMATION_SCHEMA.DATABASES
WHERE DATABASE_NAME = 'DDL_DEMO_T98';| DATABASE_NAME | COMMENT | RETENTION_TIME | IS_TRANSIENT |
|---|---|---|---|
| DDL_DEMO_T98 | episode 98 probe - dropped at the end of this file | 1 | NO |
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.
USE DATABASE DDL_DEMO_T98;| 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.
SELECT CURRENT_DATABASE() AS THE_DATABASE,
CURRENT_SCHEMA() AS THE_SCHEMA;| THE_DATABASE | THE_SCHEMA |
|---|---|
| DDL_DEMO_T98 | PUBLIC |
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.
CREATE TABLE ORPHAN_T98 (ID INT);| 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.
USE SCHEMA PUBLIC;| status |
|---|
| Statement executed successfully. |
Now a table, with a row in it whose only job is to tell one database from another later on.
CREATE OR REPLACE TABLE KEEPSAKE_T98 (ID INT, NOTE VARCHAR);| status |
|---|
| Table KEEPSAKE_T98 successfully created. |
INSERT INTO KEEPSAKE_T98 (ID, NOTE)
SELECT 1, 'this row is how we will tell the databases apart';| number of rows inserted |
|---|
| 1 |
SELECT ID, NOTE FROM KEEPSAKE_T98 ORDER BY ID;| ID | NOTE |
|---|---|
| 1 | this 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.
ALTER DATABASE DDL_DEMO_T98 RENAME TO DDL_DEMO_RENAMED_T98;| 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?
SELECT CURRENT_DATABASE() AS THE_DATABASE,
(SELECT COUNT(*) FROM DDL_DEMO_RENAMED_T98.PUBLIC.KEEPSAKE_T98)
AS ROWS_STILL_THERE;| THE_DATABASE | ROWS_STILL_THERE |
|---|---|
| NULL | 1 |
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.
SELECT CURRENT_DATABASE() AS THE_DATABASE,
CURRENT_DATABASE() = 'DDL_DEMO_RENAMED_T98' AS IT_FOLLOWED,
CURRENT_DATABASE() = 'DDL_DEMO_T98' AS IT_DID_NOT;| THE_DATABASE | IT_FOLLOWED | IT_DID_NOT |
|---|---|---|
| NULL | NULL | NULL |
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 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.
ALTER DATABASE DDL_DEMO_RENAMED_T98 RENAME TO DDL_DEMO_T98;| 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.
ALTER DATABASE DDL_DEMO_T98 SET
COMMENT = 'comment changed by ALTER'
DATA_RETENTION_TIME_IN_DAYS = 1;| status |
|---|
| Statement executed successfully. |
SELECT DATABASE_NAME, COMMENT, RETENTION_TIME
FROM DDL_DEMO_T98.INFORMATION_SCHEMA.DATABASES
WHERE DATABASE_NAME = 'DDL_DEMO_T98';| DATABASE_NAME | COMMENT | RETENTION_TIME |
|---|---|---|
| DDL_DEMO_T98 | comment changed by ALTER | 1 |
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
CREATE DATABASE IF NOT EXISTS DDL_DEMO_T98;| 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.
SELECT COUNT(*) AS ROWS_STILL_THERE FROM DDL_DEMO_T98.PUBLIC.KEEPSAKE_T98;| ROWS_STILL_THERE |
|---|
| 1 |
ROWS_STILL_THERE is 1. Nothing happened to anything. That is the safe one.
Two: plain CREATE DATABASE
CREATE DATABASE DDL_DEMO_T98;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
CREATE OR REPLACE DATABASE DDL_DEMO_T98;| 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.
SELECT COUNT(*) AS ROWS_STILL_THERE FROM DDL_DEMO_T98.PUBLIC.KEEPSAKE_T98;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.
SELECT COUNT(*) AS TABLES_IN_PUBLIC
FROM DDL_DEMO_T98.INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA = 'PUBLIC';| TABLES_IN_PUBLIC |
|---|
| 0 |
TABLES_IN_PUBLIC is 0. And the comment we set with ALTER?
SELECT DATABASE_NAME, COMMENT, COMMENT IS NULL AS COMMENT_IS_GONE
FROM DDL_DEMO_T98.INFORMATION_SCHEMA.DATABASES
WHERE DATABASE_NAME = 'DDL_DEMO_T98';| DATABASE_NAME | COMMENT | COMMENT_IS_GONE |
|---|---|---|
| DDL_DEMO_T98 | NULL | TRUE |
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 typed | What it did |
|---|---|
CREATE DATABASE IF NOT EXISTS | Nothing. The row was still there afterwards. |
CREATE DATABASE | Refused, and said the object already exists. |
CREATE OR REPLACE DATABASE | Dropped it and built a new empty one. No confirmation. |
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.
UNDROP DATABASE DDL_DEMO_T98;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 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.
USE DATABASE DDL_DEMO_T98;| status |
|---|
| Statement executed successfully. |
USE SCHEMA PUBLIC;| status |
|---|
| Statement executed successfully. |
CREATE OR REPLACE TABLE KEEPSAKE_T98 (ID INT, NOTE VARCHAR);| status |
|---|
| Table KEEPSAKE_T98 successfully created. |
INSERT INTO KEEPSAKE_T98 (ID, NOTE) SELECT 2, 'the row that survives UNDROP';| number of rows inserted |
|---|
| 1 |
Now drop the whole database, which is the one we are standing in.
DROP DATABASE DDL_DEMO_T98;| 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?
SELECT CURRENT_DATABASE() AS THE_DATABASE,
CURRENT_DATABASE() IS NULL AS IT_IS_NULL_NOW,
CURRENT_SCHEMA() AS THE_SCHEMA;| THE_DATABASE | IT_IS_NULL_NOW | THE_SCHEMA |
|---|---|---|
| NULL | TRUE | NULL |
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.
SHOW DATABASES HISTORY LIKE 'DDL_DEMO_T98';| created_on | name | is_default | is_current | origin | owner | comment | options | retention_time | dropped_on | kind | owner_role_type | object_visibility | data_quality_monitoring_settings | resharing_settings |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 2026-09-28 13:00:45.966000-07:00 | DDL_DEMO_T98 | N | N | ACCOUNTADMIN | comment changed by ALTER | 1 | 2026-09-28 13:00:52.482000-07:00 | STANDARD | ROLE | NULL | NULL | NULL | ||
| 2026-09-28 13:00:52.404000-07:00 | DDL_DEMO_T98 | N | N | ACCOUNTADMIN | 1 | 2026-09-28 13:00:55.428000-07:00 | STANDARD | ROLE | NULL | NULL | NULL |
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.
UNDROP DATABASE DDL_DEMO_T98;| status |
|---|
| Database DDL_DEMO_T98 successfully restored. |
Database DDL_DEMO_T98 successfully restored. And the row?
SELECT ID, NOTE FROM DDL_DEMO_T98.PUBLIC.KEEPSAKE_T98 ORDER BY ID;| ID | NOTE |
|---|---|
| 2 | the 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.
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.
UNDROP DATABASE DDL_DEMO_NEVER_EXISTED_T98;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
DROP DATABASE IF EXISTS DDL_DEMO_T98;| status |
|---|
| DDL_DEMO_T98 successfully dropped. |
USE DATABASE DEMO_DB;| status |
|---|
| Statement executed successfully. |
USE SCHEMA PUBLIC;| status |
|---|
| Statement executed successfully. |
And a check that we are back where we started, reading a table in DEMO_DB through an unqualified name.
SELECT CURRENT_DATABASE() AS THE_DATABASE,
CURRENT_SCHEMA() AS THE_SCHEMA,
(SELECT COUNT(*) FROM EVENTS_JSON) AS DEMO_DB_IS_READABLE;| THE_DATABASE | THE_SCHEMA | DEMO_DB_IS_READABLE |
|---|---|---|
| DEMO_DB | PUBLIC | 4 |
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
PUBLICandINFORMATION_SCHEMA. USE DATABASEselectsPUBLICfor you. I predicted otherwise and the capture proved me wrong.ALTER DATABASEdoes 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 backNULL, and nothing tells you. - The comparisons come back
NULLrather thanFALSE, becauseNULL = anythingisNULL. CREATE DATABASE IF NOT EXISTSdoes nothing to what is there. PlainCREATE DATABASErefuses and says so.CREATE OR REPLACE DATABASEdrops it and builds a new empty one, with no warning and no confirmation. The tables, the comment and the retention setting go with it.UNDROPis refused while the name is taken, with the same already exists error a plainCREATEgives.SHOW DATABASES HISTORYshows 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. DROPthenUNDROPworks 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.
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.