Migrating Off Snowflake: A Step-by-Step Guide

Snowflake is a good database. This guide exists because for a lot of teams it's also the wrong bill. If you've done the optimization pass already (right-sized warehouses, tightened auto-suspend, killed the zombie tables) and the invoice keeps growing anyway, the next question is what a migration actually looks like.
Here's the honest answer: for a typical analytics workload, it's a few weeks of methodical work, not a six-month replatforming project. Your data leaves as Parquet. Your SQL mostly ports. The hard parts are knowing what you run today and having the discipline to operate both systems in parallel before you cut over.
This is the runbook. Seven steps, in order, with the gotchas. It starts with a section on when you should not migrate, because some teams genuinely shouldn't, and figuring that out costs you an hour instead of a quarter.
One disclosure up front: Definite is an all-in-one analytics platform built on a DuckDB and DuckLake lakehouse, and it collapses steps 3 through 5 into an afternoon. The runbook itself is tool-agnostic. You can execute it with open-source parts and your own engineers.
Step 0: When you should NOT migrate
Migration talk usually starts with the bill. But the bill is only half the equation. Work through this list first:
You have heavy data sharing dependencies. If partners or customers consume your data through Secure Data Sharing or Marketplace listings, staying put is defensible. Shares are the one Snowflake feature with no lakehouse equivalent that's zero-work for the consumer. Moving means every downstream party changes how they read your data.
You have massive concurrency. Thousands of concurrent queries against one endpoint, spiky and unpredictable, is the workload multi-cluster warehouses were built for. Snowflake handles it well. A single-node engine handles a lot more than people assume, but this specific shape is Snowflake's home turf.
You're deep into Snowflake-native features. Snowpipe, Streams and Tasks, Dynamic Tables, Snowpark pipelines, Cortex functions in production paths. Each one is rebuildable. None is portable. If you count dozens of production jobs built on these, the rewrite cost can eat years of savings. Count them honestly before you start.
You haven't tried optimizing yet. Right-sizing warehouses and tightening auto-suspend have cut bills 30 to 50% for teams that had never tuned them. That's an afternoon of work. Do the cheap thing before the big thing, if only to prove to yourself the savings ceiling isn't enough.
The bill isn't actually the problem. If Snowflake costs less per month than a day of engineering time, leave it alone.
None of those apply? Keep reading. For a broader look at where teams land after Snowflake, see our Snowflake alternatives guide.
Step 1: Inventory your workloads and query patterns
You can't migrate what you can't name. Snowflake's ACCOUNT_USAGE views tell you exactly what your account does all day, and most teams have never looked.
Start with where the compute goes:
select
warehouse_name,
count(*) as query_count,
sum(total_elapsed_time) / 1000 / 3600 as total_hours
from snowflake.account_usage.query_history
where start_time > dateadd(day, -30, current_timestamp())
group by 1
order by 3 desc;
Then break query_history down by user, and join it to the sessions view (its client_application_id column) to see which tool issued each query. Classify every workload into one of four buckets:
- BI dashboards. Repeated, predictable queries from Looker, Metabase, Tableau, or whatever you run. Usually the majority of query volume and the easiest to port.
- Scheduled transformations. dbt runs, Tasks, stored procedures. Port these as a dependency graph, not a pile of files.
- Ad hoc SQL. Analysts poking around. Low risk; they follow the data.
- Applications. Anything programmatic hitting Snowflake through a driver. Highest risk, migrate last.
While you're in there, find the dead weight. ACCESS_HISTORY (Enterprise edition and up) shows which tables were actually read in the last 90 days. In our experience most accounts store far more than they query, and every table nobody reads is a table you don't have to migrate.
The output of this step is a spreadsheet: every workload, its owner, its query volume, its latency expectation, and which bucket it falls in. Boring. Also the thing that separates a three-week migration from a six-month one.
Step 2: Export to Parquet in your object store
Your data leaves Snowflake through COPY INTO, landing as Parquet files in a bucket you own:
copy into 's3://your-bucket/snowflake-export/orders/'
from analytics.prod.orders
storage_integration = your_s3_integration
file_format = (type = parquet)
header = true
max_file_size = 262144000;
Three gotchas, all learned the hard way:
- Set
max_file_size. The default is 16 MB, which fragments a large table into thousands of tiny files that every downstream engine will hate. 250 MB to 1 GB per file is a good target (5 GB is the max). - Keep
header = true. That's what preserves your column names in the Parquet schema instead ofcol1, col2, col3. - Cast
TIMESTAMP_TZandTIMESTAMP_LTZcolumns first. Snowflake errors when unloading those types to Parquet. Cast toTIMESTAMP_NTZ(and store the zone separately if you need it) before the export.
On cost: you pay normal compute for the unload queries, but data transfer is free when the destination bucket is in the same region and cloud as your Snowflake account. Put the bucket next to Snowflake. Cross-region or cross-cloud unloads pay a per-byte transfer fee.
Export in this order: the tables your BI layer reads, then transformation sources, then everything else that survived the dead-weight cut. And note what you just did, because it matters beyond the migration: your data now exists in an open format, in your bucket, readable by any engine ever written. Even if the migration stalls at this step, that copy is the cheapest insurance in data engineering.
Step 3: Stand up the lakehouse and catalog
A lakehouse is three things: Parquet files in your object store (you just made those), a catalog that tracks tables, schemas, and snapshots, and a query engine that reads them. No idle warehouse meter, no per-credit billing. Storage costs whatever your cloud provider charges for the bucket.
In Definite, this layer is DuckLake (the catalog) plus DuckDB (the engine), and the data stays in your object store as plain Parquet. Point the platform at your export bucket and register the tables. If you're assembling it yourself, the same architecture works with open-source parts; the catalog is the piece you can't skip, because without it you have a pile of files instead of tables with schema evolution and time travel.
Here's how the concepts map:
| Snowflake | Lakehouse equivalent |
|---|---|
| Database / schema / table | Schema / table in the catalog |
| Virtual warehouse | Query engine (DuckDB); no idle meter, no credits |
| Time Travel | Catalog snapshots |
| Stages | Your object store, directly |
| Snowpipe | Connectors and streaming ingest |
| Streams and Tasks | Scheduled transformations and automations |
| RBAC | RBAC |
| Secure Data Sharing | No direct equivalent (see Step 0) |
The economics of this swap are worth a paragraph. Snowflake's list price for storage is roughly what object storage costs anyway, so storage isn't where you win. The win is compute: you stop paying per-credit rates ($2 to $6 per credit, depending on edition and region) for a meter that runs whenever anything touches the data, and you stop paying the 60-second minimum every time a warehouse wakes up to serve one dashboard tile.
Step 4: Port your models and semantic layer
Your transformation SQL is the real migration surface. Good news: SQL dialects have converged, and DuckDB is close to Snowflake for analytics work. QUALIFY ports as-is. Window functions port as-is. Most dbt models port with light edits.
The edits you'll actually make:
- JSON access. Snowflake's
col:field::stringbecomes DuckDB's arrow syntax,col->>'field'. FLATTENbecomesUNNEST.IFF(cond, a, b)becomesIF(cond, a, b)or aCASEexpression.- Date function signatures differ slightly (
DATEADDandDATEDIFFvariants). Mechanical, not architectural.
Port in dependency order: staging models, then marts, then the reporting layer. After each layer, validate against Snowflake with the same two queries on both systems: row counts per table, and a sum of your key measures grouped by month. Any drift gets fixed before you move up a layer.
This is also the moment to rebuild your metric definitions in a semantic layer, once, instead of scattered across dashboard SQL. If revenue is defined in eleven places on Snowflake today, migrate one definition, not eleven. In Definite the semantic layer is built in and Fi, the AI analyst, works from it, so "which of these three revenue numbers is right" stops being a recurring meeting.
Step 5: Repoint BI and cut over consumers
Now work through the inventory from Step 1, lowest stakes first:
- Internal, low-traffic dashboards. Repoint them, let the owners bang on them for a week.
- Scheduled reports and alerts. Verify a full cycle of each (the Monday report on a Monday).
- High-visibility executive dashboards. By now you've built trust with the earlier tiers.
- Applications and embedded analytics. Last, because their failure modes are customer-facing.
Do not decommission anything yet. Snowflake keeps running through this entire step. Every consumer you repoint is reversible in minutes if something looks wrong, which is exactly why you cut over consumers one at a time instead of flipping the whole company on a Friday.
The typical experience at this step is that dashboards get faster. Dashboard queries that took seconds on a shared warehouse come back sub-second from DuckDB over partitioned Parquet. Your analysts will notice before you tell them.
Step 6: Run both in parallel and compare
Run both systems for at least one full billing cycle. This is the step impatient teams skip, and it's the one that turns a migration from a leap of faith into a decision with receipts.
Track four things, weekly:
| Metric | How |
|---|---|
| Cost | Snowflake invoice vs. object storage + platform cost |
| Latency | p95 dashboard load times on both systems |
| Parity | Row counts and key-metric sums, both systems, same day |
| Failures | Job failures and their causes on the new system |
The parity checks are non-negotiable. If numbers drift, you stop cutting over and fix the drift. A migration that ships wrong numbers fast is worse than no migration.
The cost comparison at the end of the cycle is your actual business case, with real invoices instead of estimates. That's the document you show whoever signs the contracts.
Step 7: Decommission
The satisfying part, done in order:
- Check your contract dates. If you're on a capacity contract, time the shutdown to the renewal. Prepaid credits you walk away from are real money.
- Freeze writes to Snowflake. Pause the ingestion jobs feeding it. Anything still writing to Snowflake after your consumers left is paying to update tables nobody reads.
- Run a final export. One last
COPY INTOpass for anything that changed since Step 2. This snapshot is your archive, and it's already in the open format your new system reads. - Export what history you need. Time Travel versions don't come with you. If compliance needs specific historical states, export them now or keep the account read-only until the retention window lapses.
- Cancel per the contract. Then turn off the extraction tooling, the credits, and the line item.
Keep the final Parquet snapshot in cold storage indefinitely. It costs almost nothing and it's the complete record of your Snowflake era.
The short version
Inventory what you run. Export to Parquet in your own bucket. Stand up a lakehouse with a real catalog. Port models in dependency order with parity checks. Repoint consumers lowest-stakes first. Run parallel for a billing cycle. Decommission on your contract date, not before.
Teams with straightforward analytics workloads get through this in weeks. The ones who shouldn't attempt it (heavy data sharing, extreme concurrency, dozens of Snowflake-native jobs) can find that out in Step 0, for free.
FAQ
How long does a Snowflake migration take? For a typical analytics workload (BI dashboards, scheduled transformations, ad hoc SQL), plan on a few weeks. The export and lakehouse setup take days. Porting models is the variable part, and the parallel run should cover at least one full billing cycle so you can compare real invoices. Six-month migrations happen when teams skip the inventory step and discover their dependencies one outage at a time.
Does exporting data from Snowflake cost anything? You pay normal warehouse compute to run the COPY INTO unload. Data transfer is free if the destination bucket is in the same region and cloud as your Snowflake account. Cross-region or cross-cloud unloads incur a per-byte transfer fee, so put the export bucket next to your Snowflake deployment.
What happens to Time Travel history when we leave Snowflake? It doesn't come with you. An export captures current table state, not historical versions. Going forward, a lakehouse catalog like DuckLake gives you snapshots and time travel on the new system. If you have a compliance need for old versions, export the specific historical states you need before you cancel, or keep the account read-only until the retention window lapses.
Can DuckDB really replace a Snowflake warehouse? For most analytics workloads, yes. A tuned single-node engine handles workloads into the 100 TB range, with sub-second dashboard queries on well-partitioned Parquet. Where it doesn't fit: petabyte-scale distributed joins and thousands of concurrent queries hitting one endpoint. Snowflake's multi-cluster warehouses are genuinely good at massive concurrency, and if that's your shape, keep it.
What if we depend on Snowflake data sharing? Secure Data Sharing is the strongest reason to stay. If partners consume your data through shares or Marketplace listings, there's no lakehouse equivalent that's zero-work for them. Some teams migrate internal analytics and keep a minimal Snowflake account serving the shares. If you control both sides, sharing Parquet in object storage works and is cheaper, but it's a real project for the consumers.
If you want to see the target state before committing to Step 1, Definite stands up the whole destination: connectors, the DuckLake lakehouse on your object store, BI, semantic layer, and Fi, the AI analyst. It also deploys into your own cloud via a single Helm chart if the data can't live in ours. Grab 30 minutes and bring your query_history; the inventory step is more fun with someone who's read a few hundred of them.