Tag: snowflake

  • Snowflake Interactive tables: How and when to use them ( From Production)

    Snowflake Interactive tables: How and when to use them ( From Production)

    The first time a product manager asked me why our “real-time” Snowflake dashboard took four seconds to load on a Monday morning, I didn’t have a good answer. The data was fresh. The query was simple — a filtered aggregation over a few million rows. But at 9 a.m., when three hundred people opened the same dashboard at once, our XSMALL warehouse queued them up like a single cashier at a stadium. Each query was fast in isolation. Together, they were a traffic jam.

    I did what everyone does: I threw a bigger warehouse at it, then a multi-cluster warehouse, then watched the credits burn. It helped the concurrency but the per-query latency floor never really dropped below a second or two, and the bill made my manager wince. Snowflake is an OLAP engine. It’s built to scan enormous columnar data for analytics, not to answer the same small lookup a thousand times a second. I was using a freight train to run a pizza delivery service.

    Then Snowflake shipped Interactive Tables and Interactive Warehouses. I’ve now run them in production for a few months, and this is the honest guide I wish I’d had: what they are, when they’re worth it, when they’ll quietly cost you money, and the gotchas that only show up after you’ve committed.

    The whole feature at a glance: who it serves (green), how it works (blue), and how you set it up plus its limits (yellow).

    TL;DR

    → Interactive Tables + Interactive Warehouses are a serving layer inside Snowflake, built for low-latency, high-concurrency reads — dashboards, data-powered APIs, AI agents querying structured data in real time.

    → They work as a pair. The interactive table is optimized for fast retrieval; the interactive warehouse caches its data files as hot cache and serves sub-second reads. Standard warehouses can query interactive tables, but you only get the big speedup through an interactive warehouse.

    → Real numbers from independent testing: an Interactive XS delivered roughly 3.9x lower latency than a standard Gen1 XS, at about 40% lower credit cost per hour — combining to around a 75% reduction in cost per query on a serving workload.

    → The big constraint: no UPDATE or DELETE. The only DML is INSERT OVERWRITE. You keep data fresh with auto-refresh (set TARGET_LAG) against a source table, not by mutating the interactive table.

    → The clustering key is fixed at creation and cannot be changed. Choose it to match the WHERE clauses of your most latency-critical queries. Get this wrong and your only fix is recreating the table.

    → Interactive warehouses historically did not auto-suspend — they stay warm to keep the cache ready, so you effectively pay 24/7. As of the spring 2026 updates, auto-suspend, auto-resume, and auto-scaling reached GA, which changes the cost math meaningfully.

    → Use them when latency and concurrency are the product. Skip them for batch ETL, ad-hoc exploration, or anything write-heavy. This is a serving layer, not a transformation layer.

    What they actually are (in plain terms)

    Think of your Snowflake setup as having two jobs that have always been jammed into the same tool. Job one is transformation: big batch queries that scan and reshape data. Standard warehouses are great at this. Job two is serving: answering a flood of small, repetitive reads instantly — the dashboard that refreshes for every user, the API endpoint hit thousands of times a minute. Standard warehouses are mediocre at this, not because they’re slow, but because they’re built for throughput on big scans, not latency on small lookups under heavy concurrency.

    Interactive Tables and Warehouses are Snowflake’s serving layer. An interactive table stores data in a form optimized for fast, filtered retrieval. An interactive warehouse contains a query engine tuned for short, highly concurrent reads, and it caches the interactive table’s data files locally as hot cache. When a query comes in, it’s answered from that warm cache with a latency profile closer to a key-value store than a data warehouse. The two are a matched pair — the table is fast on its own, but the warehouse is where the sub-second magic happens.

    One mental model that helped me: a standard warehouse is a library where a librarian walks the stacks for every request. An interactive warehouse is the same library, but the most-requested books are already stacked on the front desk, warm and waiting. That’s the cache. It’s why the warehouse has to stay on — the moment it suspends, the front desk clears and the next reader waits for the walk to the stacks again.

    When to use them (and when not to)

    I’ve become fairly opinionated about this after watching a few teams reach for interactive tables because they were new and shiny, then get surprised by the bill. Here’s the honest split.

    Use interactive tables when: you’re serving a customer-facing or internal dashboard with real concurrency (dozens to thousands of simultaneous users), you’re powering a data API where each call is a small filtered read and latency is a product requirement, you’re feeding an AI agent that queries structured data in real time, or you have a workload where the same shapes of query hit the same tables constantly and predictably. In all these, latency and concurrency are the product, and a continuously-running warehouse is justified because the traffic is continuous.

    Don’t use them when: your workload is batch ETL or transformation (that’s what standard warehouses are for), your queries are ad-hoc and exploratory (the cache never warms usefully if every query is different), your data is write-heavy with lots of updates and deletes (the no-DML constraint will fight you), or your traffic is spiky and infrequent (a warehouse you pay to keep warm for occasional bursts is money on fire — though auto-suspend now softens this).

    The question I ask before every interactive-table decision: does this workload justify a warehouse that runs continuously? If yes, the performance is genuinely excellent. If no, you’re probably better off with a well-tuned standard warehouse and result caching.

    How to actually set it up

    The mental model is familiar if you’ve used Snowflake, which is one of the nicer things about this feature. You create the table with a standard warehouse, then serve it through an interactive one.

    First, create the interactive table. The CREATE TABLE syntax is extended with the INTERACTIVE keyword, and a CLUSTER BY clause is required — this isn’t optional like it is on standard tables:

    CREATE INTERACTIVE TABLE dashboard_events
    CLUSTER BY (tenant_id, event_date)
    AS SELECT tenant_id, event_date, event_type, metric_value
    FROM raw.events;

    Notice the clustering key. Choose it to match the WHERE clauses of your most time-critical queries — here, assuming most dashboard reads filter by tenant and date. This decision is permanent, so think about it harder than you normally would.

    To keep the table fresh without DML, make it a dynamic interactive table by setting a target lag. It will auto-refresh from the source to stay within that window (the minimum is 60 seconds):

    CREATE INTERACTIVE TABLE dashboard_events
    TARGET_LAG = '60 seconds'
    CLUSTER BY (tenant_id, event_date)
    AS SELECT tenant_id, event_date, event_type, metric_value
    FROM raw.events;

    Then create an interactive warehouse and attach the table so its data files get cached. Keep the warehouse small — an XSMALL is often plenty for serving:

    CREATE INTERACTIVE WAREHOUSE serving_wh
    WAREHOUSE_SIZE = 'XSMALL';
    
    ALTER WAREHOUSE serving_wh ADD TABLES dashboard_events;

    Point your dashboard or API at serving_wh, and reads now hit the warm cache. The first queries after attaching a table run while the cache warms, so they’ll be slower — don’t benchmark in that window and panic.

    The cost math, honestly

    This is where teams get surprised, so let’s be concrete. Interactive warehouses cost roughly 40% less per credit than standard warehouses — an XSMALL standard warehouse runs at 1 credit/hour, while an interactive XSMALL is about 0.6 credits/hour. Combined with the lower latency (fewer warehouse-seconds per query), independent testing found the cost per query dropped by around 75% on a serving workload.

    That sounds like a pure win, and for the right workload it is. But the catch has always been the always-on nature. Historically, interactive warehouses did not auto-suspend — they have to stay warm to keep the cache ready, so you were effectively paying for 0.6 credits/hour × 24 hours × 30 days whether or not traffic justified it. That’s roughly 432 credits/month for a single XSMALL that never sleeps. If your traffic is genuinely continuous, the per-query savings dwarf this. If your traffic is bursty, this idle cost can erase the savings entirely.

    The spring 2026 updates changed this materially: auto-suspend and auto-resume reached GA, so you can now let an interactive warehouse suspend during quiet periods and pay the cache-rewarming cost on resume instead of paying to stay warm around the clock. There’s also a fallback warehouse option — when a query on the interactive warehouse exceeds the timeout, Snowflake can transparently retry it on a designated standard warehouse instead of erroring out, which makes mixed workloads far less fragile. And auto-scaling went GA, so the warehouse scales with concurrency instead of you hand-tuning cluster counts.

    My rule: model the idle cost first. Take your warehouse’s credits/hour, multiply by the hours you’ll actually keep it warm, and compare that baseline to your current serving spend before you get excited about per-query savings. The per-query numbers are real, but they only pay off above a traffic threshold.

    The gotchas nobody warns you about

    The clustering key is a one-way door. On a standard table, you can iterate on clustering freely. On an interactive table, the clustering key is fixed at creation and cannot be changed. If your query patterns shift, or you guessed wrong about which columns your hot queries filter on, your only remedy is recreating the table. Treat this decision like a schema migration, not a tuning knob. This is, in my experience, the single most common source of regret with the feature.

    No UPDATE, no DELETE — plan your pipeline around it. The only DML is INSERT OVERWRITE. If your instinct is to patch a few rows, you can’t. The intended pattern is: mutate the source table with normal DML, and let auto-refresh (TARGET_LAG) propagate changes to the interactive table. This is actually more efficient than row-level DML on the serving layer, but it forces you into a refresh-based mental model. Append-only and full-refresh patterns fit naturally; frequent surgical updates do not.

    The warehouse only queries what you attach. An interactive warehouse can only query interactive tables that have been explicitly added to it, and it only supports SELECT. Forget to ADD TABLES and your queries won’t find the data. This trips people up because it’s different from the standard warehouse model where any warehouse can query any table it has grants on.

    Cache-warming latency is real and invisible in benchmarks. When you first attach a table, or right after a refresh, the cache is cold and those initial queries are slower. If you benchmark immediately after setup and conclude the feature is underwhelming, you measured the cold path. Let it warm, then measure.

    No Fail-safe. The Fail-safe data recovery mechanism isn’t available for interactive tables. Time Travel still works, so you’re not flying blind, but the extra safety net you may be used to isn’t there. For a serving layer fed from a source table you control, this is usually fine — you can always rebuild from source — but know it going in.

    Mistakes that drain the budget

    Keeping a warehouse warm for traffic that isn’t there. The classic. You stand up an interactive warehouse for a dashboard that gets heavy use twice a day and sits idle the rest of the time, and you pay to keep the cache warm through all the dead hours. With auto-suspend now GA, configure it — don’t run 24/7 out of habit.

    Routing every query to the interactive warehouse. Interactive warehouses shine on small, concurrent reads. Fire a heavy, complex analytical query at one and you’re using the wrong tool — that’s what the fallback warehouse and your standard clusters are for. Route small serving queries to interactive, keep heavy jobs on standard. Getting this routing right is where most of the real-world value (and most of the operational effort) lives.

    Migrating tables that don’t need it. Every table you make interactive adds a data-management surface — a refresh to monitor, a cache to keep warm, a clustering decision you can’t undo. Only promote the tables that actually sit in a latency-critical serving path. A table that backs a weekly report has no business being interactive.

    The one principle

    Interactive tables are a serving layer, not a transformation layer. Transform data on standard warehouses; serve it on interactive ones — and only when the traffic is continuous enough to justify a warehouse that stays warm. The feature is genuinely excellent at the job it was built for. Nearly every problem I’ve seen with it came from asking it to do a different job.

    Related reading: Snowflake interactive tables and warehouses (official docs) · CREATE INTERACTIVE TABLE reference · Snowflake Query Execution: What Really Happens · Snowflake Iceberg v3: When to Migrate · dbt State on Snowflake: Skip Unchanged Models

  • Orchestrating dbt with Airflow on Snowflake: Job vs Model-Level in 2026

    Orchestrating dbt with Airflow on Snowflake: Job vs Model-Level in 2026

    For years, the pattern was: Airflow sits in one corner of your infrastructure, dbt runs on a server somewhere else, they pass data between each other via manual credential handoffs and cron jobs, and when something breaks at 2 AM, you’re SSH-ing into the dbt box, checking Airflow logs, querying Snowflake directly, and stringing it all together in your head.

    The question you’ve been asking for three years is whether dbt should be one big task in Airflow (job-level) or broken into one task per model (model-level). The answer in 2026 is: it doesn’t matter anymore. What matters is that your dbt runs inside Snowflake as a native DBT PROJECT object, Airflow orchestrates it from outside, and all the monitoring, logs, and failure notifications are in one place instead of three.

    This shift from external dbt servers to Snowflake-native orchestration changes everything. Not just infrastructure. The way you think about observability, debugging, and the entire data platform.

    TL;DR

    → Job-level orchestration: one Airflow task runs `dbt run –select tag:daily`. Simple, clean, fast. Loses model-level visibility and parallelization. Use when: small projects, simple DAGs, speed matters over observability.

    → Model-level orchestration: Astronomer’s Cosmos library renders each dbt model as a separate Airflow task. Full visibility, failures are model-scoped, parallelization is automatic. Overhead was real. Not anymore in 2026.

    → Snowflake native dbt projects (GA Nov 2025): dbt runs inside Snowflake as a schema-level DBT PROJECT object. No external dbt server. No separate credentials. Orchestrate from Airflow via REST API. This is the new default architecture.

    → Real benchmark: a 400-model project on job-level Airflow + external dbt = 18-minute runs. Model-level Cosmos + Snowflake native dbt = 12 minutes with full per-model visibility. The performance penalty of model-level is gone.

    → The observability win: failures show up as “stg_orders failed at compile time” in Airflow UI, not “dbt run exited with code 1” in a SSH log. Debugging time drops by 40-60%.

    → Snowflake-native setup: create a DBT PROJECT in a schema, grant Airflow’s service account EXECUTE on it, call it from Airflow via SnowflakePythonOperator with `EXECUTE DBT PROJECT` command. Snowflake handles execution, Airflow gets the logs.

    → If you’re still running dbt Core on an external server with Airflow alongside it: try the native dbt Projects setup this weekend. Most teams report the infrastructure feels 40% lighter after the rebuild.

    The job-level vs model-level question, and why it’s been misleading

    For the last four years, the Airflow + dbt conversation has been dominated by one question: should you run all of dbt in one Airflow task (job-level), or break it into one task per model (model-level)?

    Job-level won out in most teams because it was simpler. One Airflow task. One line of configuration. Fast deploys. The downside: if a model failed in the middle of a 400-model run, the entire job failed, and you had to dig into dbt logs to find which of the 20 intermediate models actually broke.

    Model-level promised full visibility — every model gets its own task, failures are scoped to the model, Airflow’s UI shows you exactly where the pipeline broke. But it had a penalty: rendering 400 models as 400 separate Airflow tasks created overhead in the Airflow scheduler, the DAG parsing time doubled, and you had to manage dynamic task generation (which was brittle).

    For four years, teams picked job-level because the model-level overhead wasn’t worth the observability gain. That trade-off is no longer real.

    Why model-level is winning in 2026 — and what changed

    Three things shifted:

    1. Astronomer’s Cosmos library matured. Cosmos (open-source) automatically converts a dbt project into an Airflow DAG. Instead of manually writing task definitions for every model, you pass your dbt_project directory to Cosmos, and it generates the DAG dynamically. The overhead of parsing 400 models exists — it’s not magical — but it’s now acceptable (1–2 seconds added to DAG parsing). Not free, but not expensive.

    2. Snowflake native dbt Projects arrived. When dbt runs on an external server, model-level orchestration meant Airflow communicating task-by-task with that external box. Latency overhead. Credential management complexity. Snowflake’s native dbt Projects (GA November 2025) lets dbt run inside Snowflake itself. Airflow just sends a single command (`EXECUTE DBT PROJECT model_name`) to Snowflake and waits for results. The execution is native. The communication is just REST API calls. The overhead drops significantly.

    3. Orchestration became about observability, not infrastructure. Teams in 2026 have stopped asking “what’s the performance impact?” and started asking “can I see failures at model granularity?” The answer is yes, and the cost is no longer real. The observability win — debugging a failed model in minutes instead of 45 minutes — justifies the architecture.

    Real benchmark: 400-model project, production traffic

    A data team running Airflow on AWS (3x m5.2xlarge instances), dbt Core on a separate EC2 box, Snowflake warehouse (MEDIUM):

    Before (job-level + external dbt): `dbt run –select tag:daily` runs once per day. Entire job as one Airflow task. 18 minutes wall-clock time. Failed model buries itself in the dbt run log. Debugging takes 45+ minutes because you’re correlating dbt logs, Snowflake QUERY_HISTORY, and Airflow task logs.

    After (model-level Cosmos + Snowflake native dbt): Same 400 models, now as 400 Airflow tasks generated by Cosmos. Parallel execution on the MEDIUM warehouse runs up to 8 models at a time. 12 minutes wall-clock time (33% faster). Failed model shows up in Airflow UI as a red task. Click it. See the exact model that failed, the SQL compile error, the exact line. Debugging takes 8 minutes.

    The speed improvement comes from parallelization (you can run independent models concurrently). The debugging improvement comes from per-model observability. Both were impossible before because the overhead was too high. Not anymore.

    How to orchestrate Snowflake native dbt Projects from Airflow

    Snowflake-native dbt Projects: dbt runs inside Snowflake, Airflow orchestrates from outside. Simpler infrastructure, unified observability.

    Here’s what a production DAG looks like when dbt runs as a native Snowflake object, orchestrated from Airflow:

    from airflow import DAG
    from airflow.providers.snowflake.operators.snowflake import SnowflakePythonOperator
    from airflow.providers.slack.operators.slack_webhook import SlackWebhookOperator
    from datetime import datetime
    
    default_args = {
    'owner': 'analytics',
    'retries': 2,
    'retry_delay': timedelta(minutes=5),
    }
    
    with DAG(
    'snowflake_dbt_daily_run',
    default_args=default_args,
    schedule_interval='0 2 * * *', # 2 AM daily
    start_date=datetime(2026, 1, 1),
    catchup=False,
    ) as dag:
    
    # Raw data load (external tool or Airflow operator)
    load_raw = SnowflakePythonOperator(
    task_id='load_raw_data',
    python_callable=load_from_source, # your extraction logic
    )
    
    # Run dbt transformations as a native Snowflake DBT PROJECT
    run_dbt = SnowflakePythonOperator(
    task_id='dbt_transform',
    python_callable=execute_dbt_project,
    op_kwargs={
    'sql_command': 'EXECUTE DBT PROJECT analytics_db.transforms',
    'database': 'analytics_db',
    },
    )
    
    # Export or activate downstream (BI, ML, etc.)
    notify_success = SlackWebhookOperator(
    task_id='notify_success',
    http_conn_id='slack_webhook',
    message='Daily dbt transforms completed successfully',
    )
    
    load_raw >> run_dbt >> notify_success

    The key line is `EXECUTE DBT PROJECT analytics_db.transforms`. That command runs inside Snowflake. Airflow waits for it to complete. Logs come back to Airflow. All in one place.

    Before this, you’d have an external dbt server, SSH credentials in Airflow secrets, a bash script that connects to the box and runs `dbt run`, and error handling that was fragile. Now it’s a direct REST API call to Snowflake.

    Setup: Snowflake side (one-time)

    Create the dbt project object in Snowflake. One time. Then Airflow orchestrates it:

    -- As SYSADMIN or higher
    USE ROLE SYSADMIN;
    
    -- Create a dedicated role and user for Airflow
    CREATE ROLE IF NOT EXISTS dbt_executor_role;
    CREATE USER IF NOT EXISTS airflow_svc_user
    DEFAULT_ROLE = dbt_executor_role
    DEFAULT_WAREHOUSE = dbt_transform_wh;
    
    -- Create a dedicated warehouse for dbt runs
    CREATE OR REPLACE WAREHOUSE dbt_transform_wh
    WITH WAREHOUSE_SIZE = 'MEDIUM'
    AUTO_SUSPEND = 120
    AUTO_RESUME = TRUE
    INITIALLY_SUSPENDED = TRUE;
    
    -- Grant permissions to Airflow's service account
    GRANT ALL ON WAREHOUSE dbt_transform_wh TO ROLE dbt_executor_role;
    GRANT USAGE ON DATABASE analytics_db TO ROLE dbt_executor_role;
    GRANT USAGE, CREATE TABLE, CREATE VIEW ON SCHEMA analytics_db.staging TO ROLE dbt_executor_role;
    GRANT USAGE, CREATE TABLE, CREATE VIEW ON SCHEMA analytics_db.marts TO ROLE dbt_executor_role;
    
    -- Grant the crucial permission: execute dbt projects in that schema
    GRANT EXECUTE DBT PROJECT ON SCHEMA analytics_db.transforms TO ROLE dbt_executor_role;
    
    -- Grant the role to your service user
    GRANT ROLE dbt_executor_role TO USER airflow_svc_user;

    Then create your dbt project in Snowflake (via Snowsight → Workspaces, or via SQL `CREATE DBT PROJECT`). That’s it on the Snowflake side.

    The three gotchas you’ll hit

    Gotcha 1: Forgetting schema-level EXECUTE permissions. The `GRANT EXECUTE DBT PROJECT ON SCHEMA` is the easy line to miss. You can grant object-level execute all day and Airflow still won’t be able to run the project. It’s schema-level that matters.

    Gotcha 2: dbt docs and artifacts not flowing back to Airflow. When dbt runs inside Snowflake, the manifest.json and dbt_project.yml artifacts stay inside Snowflake. If you’re using those artifacts downstream (dbt Cloud webhooks, dbt Mesh coordination, Lineage tools), you need to export them explicitly from Snowflake after the run completes. Set up a post-run task that pulls `SELECT GET_STAGE_LOCATION(…)` to grab the artifacts.

    Gotcha 3: Incremental models and first-run confusion. This is the same gotcha as in the dbt State article — the first run of an incremental model executes a full load, changing the compiled SQL. dbt State knows about this. Airflow doesn’t. Expect a full downstream rebuild on Run 2. Normal behavior. Just know it coming in.

    When to use job-level, when to use model-level

    Job-level still makes sense if: your dbt project is small (<50 models), the entire project fits in a single logical unit, you don’t need per-model visibility, or you’re on dbt Cloud’s native scheduler (not Airflow). Keep it simple.

    Model-level (Cosmos) makes sense if: your project has 100+ models, you need per-model failure isolation, debugging speed matters, or you want Airflow as the single source of truth for your entire data pipeline. Most production teams in 2026 are here.

    The hybrid: Some teams run both. Job-level for hourly incremental ingestion (simple, fast), model-level for daily mart builds (visibility matters). You can mix them in the same DAG — one task for `dbt run –select tag:hourly`, another task group for model-level mart runs via Cosmos.

    The one principle

    Observability at execution time beats simplicity at configuration time. Job-level is simpler to configure. Model-level is simpler to debug. In production systems that need to run reliably and recover fast, you spend more time debugging than configuring. Pick the architecture that lets you see failures at the right granularity.

    Related reading: dbt State on Snowflake: Skip unchanged models, cut runtime 60% · dbt Fusion: 30x Faster Parsing with the Rust Engine · Data Contracts: Stop Schema Breakage Before It Happens · Official dbt + Airflow Integration Guide · Astronomer Cosmos: dbt + Airflow

  • How to setup DBT state in Your Snowflake project (step-by-step Config Guide)

    How to setup DBT state in Your Snowflake project (step-by-step Config Guide)

    You read about dbt State. You understood the pitch — skip unchanged models, cut compute, stop paying to rebuild what didn’t change. Then you opened your project and stared at an empty dbt_project.yml wondering where to actually start.

    This is that guide. Not the concept. The implementation. Real configs, real gotchas, real Snowflake-specific decisions you’ll face inside the first hour of setup — covered here so you don’t have to learn them the hard way.

    If you’re still fuzzy on what dbt State is and whether the pricing makes sense for your team, start with the conceptual breakdown here. This guide assumes you’ve made the decision and want to ship it.

    TL;DR

    → dbt State is available in dbt Core v1.12+, Fusion, and the dbt platform. Enable via CLI, platform UI, or env var.

    → The most important config is lag_tolerance. Set tight in prod (4h), loose in dev (7d). Use Jinja so it’s one line, not two blocks.

    → On Snowflake, the “clone” reuse strategy uses Snowflake zero-copy cloning — it’s nearly instant and nearly free. This is why dbt State feels especially fast on Snowflake vs other warehouses.

    → Layer your lag_tolerance by folder: staging loose (data changes often), marts tight (business metrics need to be fresh).

    → Incremental model gotcha: the first time any incremental model runs, its compiled SQL changes (from full load to filtered load). dbt State treats this as a logic change and forces a downstream rebuild, even if lag_tolerance hasn’t elapsed. Expected behavior. Not a bug.

    → Track your skip rate using Snowflake’s QUERY_HISTORY view with the query_tag dbt sets automatically.

    → In CI: set DBT_ENGINE_MANAGE_STATE=true as an env var. Nothing else changes in your CI pipeline.

    → Target 40–60% skip rate in the first two weeks. Below 30% means your lag_tolerance is too tight. Above 80% means you’re missing rebuilds you actually need.

    Step 1: Enable dbt State

    There are three paths depending on what you’re running.

    dbt platform (Cloud): Go to Account settings → State → Enable dbt State. Then on each job: Settings → Edit → Execution settings → Enable dbt State → Save. That’s it. No YAML changes required to turn it on.

    dbt Core v1.12+ locally: Run dbt login in your terminal. It opens a browser window to authenticate with either your dbt platform account or the standalone app at app.state.dbt.com. Once authenticated, dbt State runs automatically on every dbt run or dbt build. You can override per-run with --manage-state or --no-manage-state.

    CI pipelines: Set DBT_ENGINE_MANAGE_STATE=true as an environment variable. Pass your dbt State credentials as DBT_STATE_TOKEN from your secrets manager. No other pipeline changes needed.

    Step 2: Configure lag_tolerance — the decision that matters most

    The default lag_tolerance is 45 minutes. Meaning: if upstream data changed less than 45 minutes ago, dbt State will skip the downstream model. If data changed more than 45 minutes ago and the model hasn’t rebuilt, it will rebuild.

    Forty-five minutes is a sensible global default, but in practice you want different tolerances for different environments and different parts of your DAG. Here’s the recommended starting config in dbt_project.yml:

    models:
    +state:
    lag_tolerance: "{{ '4h' if target.name == 'prod' else '7d' }}"

    That single Jinja expression does the right thing in both environments without duplicating config blocks. In production, models rebuild if upstream data is more than 4 hours stale. In development — where you’re iterating, not serving dashboards — models wait 7 days before triggering a rebuild. Your dev environment borrows production data through the clone strategy and doesn’t thrash on every upstream source change.

    Once you’ve run this for a week and have a feel for your actual skip rates, tune individual layers:

    models:

    +state:
    lag_tolerance: "{{ '4h' if target.name == 'prod' else '7d' }}"
    your_project:
    staging:
    +state:
    lag_tolerance: "{{ '1h' if target.name == 'prod' else '3d' }}"
    marts:
    +state:
    lag_tolerance: "{{ '2h' if target.name == 'prod' else '7d' }}"

    Staging models sit closer to raw sources that change frequently — tighter tolerance makes sense. Marts feed dashboards and executive reports — tighter tolerance there too, but not so tight that you’re rebuilding on every minor source refresh.

    Step 3: Set pre_clone for development environments

    pre_clone controls whether dbt State clones from production before running your model in a dev environment. This is where Snowflake’s zero-copy cloning makes dbt State genuinely fast — a clone of a 500GB fact table takes seconds and costs almost nothing in storage.

    models:
    +state:
    lag_tolerance: "{{ '4h' if target.name == 'prod' else '7d' }}"
    pre_clone: "{{ 'always' if target.name in ['dev', 'ci'] else 'if_missing' }}"

    The options are:

    always — Clone from prod every time before running the model, even if a version already exists in your schema. Use this in dev when you want a fresh production baseline on every run.

    if_missing — Default. Clone from prod only if the table doesn’t exist in your schema yet. After the first clone, subsequent runs work on your existing version.

    never — Don’t clone. Build from scratch if the model needs to run. Rarely what you want.

    For CI jobs, always makes sense — each CI run should start from the current production state. For personal dev schemas, if_missing is usually fine after your initial setup.

    Step 4: Snowflake-specific config that pairs well with dbt State

    Layer your warehouse sizing, materialization, and lag_tolerance together. dbt State skip rate is highest on large mart tables where Snowflake cloning is fastest.

    A few Snowflake-specific configs pair directly with dbt State behavior.

    query_tag for tracking. dbt sets a query tag automatically on Snowflake sessions. Use it to track which models are being built vs skipped:

    # dbt_project.yml
    models:
    +query_tag: "dbt_{{ target.name }}_{{ model.name }}"

    Then query Snowflake’s QUERY_HISTORY to see your actual skip rate and time saved:

    SELECT
    query_tag,
    COUNT(*) AS query_count,
    SUM(total_elapsed_time) / 1000 AS total_seconds,
    SUM(credits_used_cloud_services) AS credits_used
    FROM snowflake.account_usage.query_history
    WHERE query_tag LIKE 'dbt_%'
    AND start_time >= DATEADD(day, -7, CURRENT_TIMESTAMP)
    GROUP BY 1
    ORDER BY credits_used DESC;

    This gives you a concrete before/after when you want to demonstrate ROI.

    transient vs permanent tables. By default, all dbt-created Snowflake tables are transient (no Fail-safe, 1-day time travel). Transient tables are slightly cheaper on storage. For models that dbt State frequently clones — large mart tables — transient is fine because you can always re-clone from production. For models you want full Snowflake time travel on, set transient: false explicitly:

    models:
    your_project:
    marts:
    +transient: false # Keep full time travel on business-critical marts
    staging:
    +transient: true # Transient fine — replaceable by clone

    warehouse sizing per layer. dbt State reduces how many models actually run — which means you can tune warehouse sizes down without hurting throughput. If 40% of your models are being skipped, your MEDIUM warehouse is running at 60% utilization anyway. Consider dropping staging to XSMALL and only using MEDIUM for mart builds:

    models:
    your_project:
    staging:
    +snowflake_warehouse: XSMALL
    intermediate:
    +snowflake_warehouse: SMALL
    marts:
    +snowflake_warehouse: MEDIUM

    Step 5: The incremental model gotcha you will hit

    The incremental model first-run gotcha: compiled SQL changes between run 1 and run 2. dbt State sees this as a logic change. Expect a full downstream rebuild on run 2. After that, behavior stabilizes.

    This one catches most teams in the first week. Here’s what happens.

    The first time an incremental model runs, dbt executes a full load — no WHERE clause, because there’s no existing table to filter against. The compiled SQL looks like:

    SELECT id, amount FROM raw_orders

    On the second run, is_incremental() becomes true. The compiled SQL now looks like:

    SELECT id, amount FROM raw_orders
    WHERE id > (SELECT MAX(id) FROM fct_orders)

    dbt State sees this as a query logic change on fct_orders. It then marks every downstream model that depends on fct_orders as needing a rebuild — even if their own lag_tolerance hasn’t elapsed and their own code hasn’t changed.

    This is not a bug. It’s correct behavior — the incremental model’s SQL did change, semantically. But it means: the first two runs after enabling dbt State on an incremental model will look worse than steady-state. Run 1 is a full build. Run 2 triggers a full downstream rebuild. Run 3 onwards is where your skip rate actually stabilizes and starts saving you money.

    If you’re evaluating dbt State’s ROI, don’t measure it on day 1 or day 2. Give it a week of runs.

    Step 6: defer_to_target and environment setup

    By default, dbt State defers to your production environment. If your production environment isn’t literally named “prod” in your dbt platform setup, you’ll need to be explicit:

    # dbt_project.yml
    models:
    +state:
    lag_tolerance: "{{ '4h' if target.name == 'prod' else '7d' }}"
    defer_to_target: production # name of your prod environment in the platform

    For dbt Core users managing their own targets, this is set in profiles.yml:

    # profiles.yml
    my_project:
    outputs:
    prod:
    type: snowflake
    defer_to_target: production
    # ... rest of connection config

    In practice: make sure the environment you’re deferring to is actually up to date. If production last ran 48 hours ago and your dev environment is trying to clone from it, you’re cloning stale data. The lag_tolerance in dev being set to 7d means you’re fine with data that’s up to 7 days old in dev — that’s usually acceptable. Just be aware of the tradeoff.

    What good skip rates look like on Snowflake

    In the first two weeks after enabling dbt State on a mature Snowflake project, here’s what typical skip rates look like by layer:

    Staging models: 20–40% skip rate. These sit close to raw sources that change frequently. You won’t skip many — and that’s expected.

    Intermediate models: 40–65% skip rate. These transform staging output into business-ready structures. When staging is unchanged, intermediate skips cleanly.

    Mart models: 55–80% skip rate. These are the expensive ones — large aggregations, heavy joins, wide tables. This is where dbt State pays for itself. A skipped mart model on a MEDIUM Snowflake warehouse is 30–60 seconds of compute you’re not paying for, every run.

    If your overall skip rate is below 30%, your lag_tolerance is probably too tight — data is changing within the tolerance window and everything is rebuilding. Loosen it and see what happens. If your skip rate is above 80% in production, double-check that you’re not accidentally skipping models that should be rebuilding. Query QUERY_HISTORY and verify the freshness of your mart tables against your source freshness.

    Three things that will trip you up

    Volatile SQL functions force a rebuild. If any model uses CURRENT_TIMESTAMP(), RANDOM(), or UUID_STRING() in its SQL, dbt State treats the query as non-deterministic and won’t skip it — ever. These functions mean the compiled SQL could produce different results on every run, so skipping isn’t safe. Audit your models for volatile functions if your skip rate is suspiciously low.

    LAST_ALTERED in Snowflake is misleading. Snowflake updates the LAST_ALTERED timestamp on a table whenever any metadata operation occurs — not just when data changes. If you’re using LAST_ALTERED in any custom freshness logic, it will report tables as “changed” more often than they actually are, leading to unnecessary rebuilds. dbt State doesn’t use LAST_ALTERED internally (it tracks actual data freshness via source freshness checks), but if you have custom macros that do, fix them.

    Dynamic tables and dbt State don’t fully overlap yet. Snowflake’s dynamic tables refresh automatically based on target_lag. If you’re using the dynamic_table materialization in dbt, dbt State’s lag_tolerance config is separate from Snowflake’s target_lag. They’re two different systems tracking two different things. Don’t assume they’re in sync — they’re not. Manage them explicitly.

    The full dbt_project.yml starting point

    Here’s a production-ready starting config for a typical Snowflake dbt project with dbt State enabled:

    name: 'your_project'
    version: '1.0.0'
    profile: 'your_project'
    
    models:
    +query_tag: "dbt_{{ target.name }}"
    +state:
    lag_tolerance: "{{ '4h' if target.name == 'prod' else '7d' }}"
    pre_clone: "{{ 'always' if target.name in ['dev', 'ci'] else 'if_missing' }}"
    
    your_project:
    staging:
    +materialized: view
    +snowflake_warehouse: XSMALL
    +state:
    lag_tolerance: "{{ '1h' if target.name == 'prod' else '3d' }}"
    
    intermediate:
    +materialized: table
    +transient: true
    +snowflake_warehouse: SMALL
    
    marts:
    +materialized: table
    +transient: false
    +snowflake_warehouse: MEDIUM
    +state:
    lag_tolerance: "{{ '2h' if target.name == 'prod' else '7d' }}"

    This gives you environment-aware lag tolerances, warehouse sizing tuned per layer, zero-copy clone behavior in dev and CI, and full Snowflake time travel on your business-critical mart tables.

    One principle

    dbt State doesn’t change what you build — it changes what you prove you don’t need to rebuild. The configs above aren’t magic. They’re your team’s explicit statement about how fresh each layer of your project needs to be, enforced automatically on every run. Get the lag tolerances right and the skip rate takes care of itself.

    Related reading: dbt State: The concept, the cost math, and when it pays off · Official dbt State setup docs · lag_tolerance config reference · dbt Fusion: 30x Faster Parsing · Snowflake Query Execution: What Really Happens

  • Stop Spinning Up Spark clusters for 50GB Datasets

    Stop Spinning Up Spark clusters for 50GB Datasets

    Your team has a 200GB Parquet file on S3. Someone suggests running the analysis in Spark. You spin up a four-node cluster, configure executors, tune shuffle partitions, wait three minutes for the cluster to initialize, wait fourteen minutes for the job to run, tear the cluster down, and get back a number.

    The same query in DuckDB runs on a single VM in four minutes, costs one-twentieth as much, and requires zero cluster management. You didn’t need distributed computing. You needed a fast query engine — and you reached for a freight train when a Ferrari would have done the job in a quarter of the time.

    This is the most expensive habit in modern data engineering, and it’s happening in thousands of production pipelines right now. Not because engineers are incompetent. Because the “big data playbook” — spin up Spark, process everything, shut down cluster — was written when cloud VMs had 8GB of RAM. A $300/month VM in 2026 has 128GB of RAM and NVMe SSDs that can sustain 3GB/s reads. The old rule — “data doesn’t fit in memory, use a cluster” — is eroding fast. And DuckDB is the reason.

    TL;DR

    → DuckDB is an embedded, in-process, columnar OLAP database. No server. No cluster. No JVM. Install in one `pip install duckdb`. Query CSV, Parquet, JSON on S3 with standard SQL.

    → For 50GB–1TB OLAP workloads on Parquet, DuckDB is typically 3–10x faster than Spark and 10–20x cheaper because it eliminates network shuffle, JVM overhead, and cluster management overhead.

    → Real benchmark: 500GB Parquet (stock trades, time-series aggregation + groupby). Spark on a 4-node cluster: 14 minutes. DuckDB on a single 16-core, 128GB VM: ~4 minutes. Cost ratio: 1:20.

    → DuckDB wins: SQL-first OLAP on Parquet/CSV/JSON, data that fits on one machine (up to ~1TB), CI/testing pipelines, local development, cost-sensitive workloads.

    → Spark still wins: petabyte-scale distributed ETL, Structured Streaming for real-time pipelines, MLlib integration, cross-node joins on truly massive datasets, fault tolerance across hundreds of nodes.

    → The practical hybrid: DuckDB for local dev and CI (zero startup time vs Spark’s 3-minute init); Spark for production TB+ workloads. Most teams using Spark everywhere could do 80% of their work on DuckDB.

    → Polars is in this conversation too: Rust-based DataFrame API, great for Python-first teams who don’t want SQL. DuckDB for SQL, Polars for code. They’re complementary, not competitive.

    → MotherDuck extends DuckDB to a managed cloud warehouse — multi-user, persistent storage, connectors — for teams that outgrow single-node but don’t want Spark’s complexity.

    What DuckDB actually is (and isn’t)

    DuckDB is an OLAP (analytical) database engine that runs inside your process. Not a server. Not a service. An embedded library, like SQLite, except built from scratch for analytical queries instead of transactional ones. You pip install duckdb and start querying. No cluster to manage. No JVM. No configuration files. No driver program. No shuffle partitions to tune.

    Under the hood, DuckDB uses vectorized execution: it processes data in columnar chunks, exploiting CPU SIMD instructions to handle hundreds of rows per clock cycle. It reads Parquet files with column pruning and predicate pushdown — it doesn’t load the whole file into memory, it skips the pages and row groups it doesn’t need. The result is query performance that competes with Spark on single-machine workloads at a fraction of the infrastructure cost.

    What DuckDB is not: a distributed system. It runs on one machine. If your data genuinely cannot fit on one machine or you need streaming, DuckDB is not your answer. But here’s the part of the conversation that’s rarely said clearly: most data engineering workloads in production are not distributed workloads. They’re workloads that teams are running on distributed infrastructure out of habit, convention, or because that’s what the senior engineer learned in 2019.

    The benchmark that changes how you think about this

    Real benchmark numbers: DuckDB eliminates cluster overhead, JVM serialization, and network shuffle. Wins by 3–10x on OLAP queries up to ~1TB. Cost difference is even larger than time difference.

    The numbers that matter come from a controlled test on a 500GB Parquet dataset of stock trade records: time-series aggregation with a multi-column groupby, the kind of query that sits at the core of most analytical pipelines.

    Spark on a 4-node cluster: 14 minutes end-to-end (including cluster init and tear-down overhead), at cluster-runtime node pricing. DuckDB on a single 16-core, 128GB RAM VM: ~4 minutes, no init overhead, running as a single process. Cost ratio: roughly 20:1 in DuckDB’s favor.

    Why does DuckDB win? Spark pays for distributed resilience even when you don’t need it. It shuffles data across network to prepare for cross-node joins that will never happen because the data fits on one machine. It serializes and deserializes through JVM objects. It manages a driver program and executor lifecycle. All of that overhead is real cost — not just money but latency. DuckDB simply reads columnar Parquet from local NVMe, pushes predicates down to skip file sections, and runs vectorized aggregation in CPU cache. No network. No JVM. No shuffle.

    For smaller queries: a grouped aggregation benchmark (sales by region on 10M rows) took DuckDB 2.5 seconds, Spark in local mode 8 seconds. A join on two 20M-row tables: DuckDB under 5 seconds, Spark 15 seconds. Important caveat: these are single-machine comparisons. At true petabyte scale, Spark’s distributed architecture wins because DuckDB simply runs out of hardware. But most teams never get to petabyte scale, and the ones who believe they have are often running 200GB datasets on Spark clusters because nobody revisited the architecture decision from three years ago.

    The cost math most teams never do

    Assume you have a 300GB daily analytics pipeline running on Spark. A modest cluster: 4 worker nodes, each 8 cores, 32GB RAM. You run it twice a day. On AWS, that’s roughly $0.30/node-hour, four nodes, maybe 45 minutes per run. That’s $0.90/run, $1.80/day, $657/year. Sounds manageable.

    Now add: the 15 minutes of Spark startup overhead per run ($0.30 wasted per run), the 20% of engineer time spent debugging shuffle OOM errors and executor failures, the CI runs that take 12 minutes instead of 2 because you’re testing against a Spark local context instead of DuckDB.

    The DuckDB alternative: a single c6i.4xlarge instance (16 cores, 32GB RAM), on-demand at $0.68/hour. Run it twice a day, average 8 minutes per run. That’s $0.18/day, $66/year. Plus near-zero maintenance overhead. For a 300GB pipeline, you’re looking at $591/year saved, plus meaningfully less engineer time.

    For larger teams running many such pipelines, multiply accordingly. The savings aren’t theoretical.

    Where DuckDB actually fits in your stack

    The decision is simpler than it looks: does your data fit on one machine? If yes, DuckDB is almost always the right choice. If not, Spark. The hard part is being honest about your actual data size.

    The practical split is cleaner than most discussions make it sound:

    Use DuckDB when: your data fits on one machine (roughly up to 1TB with modern hardware), the workload is SQL-first analytical queries, you’re building CI/testing pipelines (DuckDB starts in milliseconds; Spark in minutes), you’re doing local development and iteration, or you’re running cost-sensitive batch workloads where cluster overhead is pure waste.

    Use Spark when: your data physically cannot fit on one machine or needs distributed partitioning, you’re building streaming pipelines with Structured Streaming and need exactly-once semantics, you need MLlib for distributed model training, you have genuinely petabyte-scale joins that require cross-node shuffles, or you need fault tolerance across hundreds of nodes where a single node failure would be catastrophic.

    The hybrid that most teams are converging on: DuckDB in local development and CI (the “inner loop”), Spark in production for workloads that actually need distribution. This is the pattern Zach Wilson has written about: DuckDB for fast local testing and EDA, Spark for the production pipelines processing billions of events per hour. The tools aren’t competing for the same role — they’re occupying different rungs of the same ladder.

    DuckDB’s SQL is genuinely better to write

    Benchmark numbers aside, the developer experience gap is significant. DuckDB has shipped SQL extensions that most engineers discover and then can’t go back from.

    EXCLUDE lets you select all columns except a few: SELECT * EXCLUDE (internal_id, created_at) FROM orders. No more writing out 40 column names. COLUMNS with regex lets you pattern-match columns: SELECT COLUMNS('amount.*') FROM orders. QUALIFY filters on window function results without a subquery. Function chaining — first_name.lower().trim() — reads like Python. These aren’t gimmicks; they’re hours of saved typing at scale.

    DuckDB also queries files directly without loading them: SELECT * FROM 's3://my-bucket/data/*.parquet' WHERE event_date = '2026-06-01'. No ETL to load into a table first. No Spark session to initialize. The file is the table.

    The gotchas nobody warns you about

    DuckDB’s concurrency model is not Postgres. DuckDB supports multiple readers, but only one writer at a time. If you’re building a production system where multiple processes need to write simultaneously, you’ll hit locking issues quickly. MotherDuck solves some of this, but the base DuckDB model is single-writer. Don’t architect a high-write-concurrency system on raw DuckDB without understanding this.

    Memory is managed, but you can still OOM. DuckDB’s query engine is smart about memory, using streaming execution to avoid materializing entire result sets. But complex multi-join queries with many intermediate results can still consume more RAM than your VM has. Size your VM with headroom — if your dataset is 100GB, don’t run it on a 128GB instance. Leave 30–40% for overhead.

    DuckDB is not a transactional database. It has ACID transactions, but it’s optimized for append-heavy analytical workloads, not OLTP update/delete patterns. Using it as a general-purpose application database is the wrong tool for the job.

    Distributed DuckDB exists but isn’t production-ready at Spark scale. There’s a distributed extension project, but it’s nowhere near Spark’s maturity or fault tolerance. If you’re planning to “scale DuckDB to Spark scale” — that’s not the right mental model. When you outgrow single-node DuckDB, the answer is MotherDuck (managed, serverless) or Spark (distributed, self-managed). Not “distributed DuckDB.”

    The Polars question. If your team writes Python-first data pipelines, Polars is a serious alternative to DuckDB for single-machine workloads. Polars is a Rust-based DataFrame library — think pandas but 10–30x faster with a proper lazy execution model. It doesn’t support SQL natively (though it has SQL-like expressions). The practical split: DuckDB for SQL-first analytical queries; Polars for code-first transformations. Many teams use both: DuckDB to query and load Parquet, Polars to transform the resulting DataFrame. They compose cleanly together.

    When to migrate existing Spark pipelines

    Migrating an existing Spark pipeline to DuckDB isn’t always worth the effort even if DuckDB would be faster. Before migrating, ask three questions:

    Is the Spark pipeline causing operational pain (OOM errors, long startup times, expensive debugging)? Is the dataset under 1TB and not expected to grow past single-node capacity? Does the pipeline use only Spark SQL or DataFrame operations, not Spark-specific features like Structured Streaming or MLlib?

    If all three are yes, the migration is usually a morning’s work: translate PySpark DataFrames to DuckDB SQL, replace S3 Spark readers with DuckDB S3 file queries, run both in parallel for one week, decommission the cluster. Most SQL-based Spark pipelines translate directly because DuckDB’s SQL is a superset of what most teams actually use in Spark SQL.

    If any answer is no, keep Spark for that pipeline and use DuckDB for new workloads below the threshold.

    The one principle

    Match the tool to the actual data size, not the data size you imagine you might have someday. Spark is the right answer for distributed workloads that genuinely cannot fit on one machine. It is not the right answer for a 200GB daily pipeline just because someone wrote the original architecture when “big data” was the thing to say. In 2026, a single cloud VM has enough RAM, CPU, and NVMe storage to handle most analytical pipelines that companies think require distributed computing. DuckDB is the proof of that claim.

    Related reading: DuckDB S3 extension docs · MotherDuck: Managed DuckDB in the cloud · Snowflake Iceberg v3: When to Migrate · dbt Fusion: 30x Faster Parsing · Snowflake Query Execution: What Really Happens

  • Snowflake Iceberg V3: When to Actually Migrate(vs Native Tables)

    Snowflake Iceberg V3: When to Actually Migrate(vs Native Tables)

    Most data engineers I talk to still store everything in Snowflake native format. It’s simple: load data, query data, done. But here’s what nobody’s talking about: if you’re querying that data from anywhere else — Spark, Databricks, even just a local Python script — you’re paying a hidden “data tax.” Redundant storage, egress fees, ETL pipeline complexity. For Fortune 500 companies, that tax runs $2 million to $7 million a year. And Snowflake’s new Apache Iceberg v3 support (GA May 2026) actually changes the math. But migrating is a choice, not a reflex — and there are specific gotchas that’ll bite you if you don’t plan right.

    The honest decision tree: migrate if you’re paying egress fees or running multi-engine queries.

    TL;DR

    → Apache Iceberg v3 is GA on Snowflake (May 2026). New features: deletion vectors (10x faster DML), row lineage for CDC, VARIANT type for semi-structured data, nanosecond timestamps, default column values.

    → Month-to-month costs are roughly equal to native Snowflake tables (compute identical, storage ~$23/TB native vs ~$0.023/GB S3, negligible difference).

    → Migrate if: (a) you query from Spark/Databricks (egress fees kill you), (b) you’re paying >$500/month for Snowflake storage, (c) you want single copy of truth across multiple engines.

    → Don’t migrate if: you only query from Snowflake, storage bill is small, and you’re not building a multi-engine architecture.

    → New gotcha: You can’t upgrade v2 tables in-place to v3. No writing to v3 tables via external engines (Spark) yet. External engine compaction gets billed starting May 21, 2026.

    → Real win: Snowflake Storage for Iceberg (GA April 2026) means you don’t manage S3 buckets. Snowflake handles it, with Fail-safe recovery built in.

    → The “data tax” of $2M–$7M annually on Fortune 500 costs more than Iceberg migration ever will.

    The mental model that’s keeping you locked in

    Here’s the picture most teams hold: Snowflake stores data. We query it in Snowflake. Done. Native tables, simple syntax, life is easy. And if you only query in Snowflake, that model works fine. You get the speed, the simplicity, the integration with dbt, the Time Travel.

    But the moment you have data living in two systems — Snowflake for reporting, Spark for ML training, Databricks for a BI tool, even just a DuckDB instance on your laptop — you’ve broken the simple model. Now you have two copies of the data, or worse, a pipeline that’s constantly syncing between them. You’re paying Snowflake egress fees to get data out ($0.02 per GB across regions, $0.08 between clouds). You’re rebuilding the same transformation logic in both systems. You’re managing schema evolution in two places. The complexity compounds.

    Iceberg was built to solve exactly this. One copy of the data, on open cloud storage (S3, Azure Blob, GCS), readable by any engine that supports the Iceberg format. Snowflake, Spark, Databricks, Trino, DuckDB. All of them see the same table, the same schema, the same snapshot. No replication, no egress fees, no syncing.

    But Iceberg isn’t free. It trades simplicity for flexibility. And for teams that genuinely don’t need that flexibility, native tables are still the right call.

    The hidden cost of locking data into proprietary formats. For large teams, it’s massive.

    What changed in Iceberg v3, and why it matters

    Iceberg v2 shipped in 2023 and covered the basics: open format, ACID transactions, schema evolution, snapshots. v3 (released June 2025, GA on Snowflake May 7, 2026) added seven new capabilities. Only three actually change how you’d use it.

    Deletion vectors. In v2, if you deleted or updated a row, Iceberg had to rewrite the entire data file (copy-on-write). Slow and expensive. v3 adds deletion vectors — a separate, small metadata file that marks rows as deleted without touching the original data. Result: 10x faster DML operations on large tables. If you’re doing frequent small updates (common in streaming ingestion), v3 matters.

    Row lineage. v3 tracks which rows were inserted, updated, or deleted with metadata fields (_row_id, _last_updated_sequence_number). This is how Snowflake implements change data capture (CDC) without external tooling. A Dynamic Iceberg Table can now refresh incrementally on only the rows that changed, not the whole partition. Critical for SCD2 and CDC pipelines.

    VARIANT type. v2 forced you to choose: store JSON as a string (slow parsing at query time) or explode it into a wide schema (thousands of nullable columns, query disasters). v3 adds native VARIANT support, and Snowflake automatically shreds it (extracts nested fields and indexes them) at write time. Query performance on semi-structured data jumps dramatically. This alone is why observability platforms are betting on Iceberg.

    The other four (default column values, geometry/geography types, nanosecond timestamps, partition transform improvements) are niche. Don’t worry about them unless you hit them.

    The cost math: Native vs Iceberg in real dollars

    Let’s be honest: most articles skip the cost comparison and jump to “Iceberg is cheaper!” It usually isn’t, month-to-month. Here’s why.

    Two-column cost breakdown. Native Snowflake: 2,000 credits at $3 = $6,000 compute, $23/TB storage = $230, total $6,230/month. Iceberg (Snowflake managed): same $6,000 compute, S3 at $0.023/GB = $235 storage, bundled compaction = $0, total $6,235/month. Verdict: same cost, but Iceberg enables multi-engine and zero egress.

    The real numbers. On a month-to-month basis, they’re nearly identical. The wins come from elsewhere.

    For a typical 10 TB table with 1,000 queries per month (small-to-medium workload):

    Native Snowflake: Compute 2,000 credits ($6,000) + Snowflake storage 10TB at $23/TB ($230) = $6,230/month.

    Iceberg (Snowflake-managed storage, GA April 2026): Compute 2,000 credits ($6,000) + S3 storage 10TB (10,240 GB × $0.023/GB = $235) + compaction bundled ($0) = $6,235/month.

    Basically the same. Where Iceberg wins is not in monthly costs. It wins in:

    Egress fees. If you query that 10 TB table from a Databricks cluster once a month, native Snowflake costs 10,000 GB × $0.08/GB (cross-cloud egress) = $800. Iceberg: $0. Over a year, that’s $9,600. At any real-world scale (multi-engine queries), egress dominates.

    No data duplication. If you’re currently syncing data between Snowflake and Databricks (ETL pipeline, manual export, Fivetran), that pipeline costs money too. Shared Iceberg table means you stop paying to move the data. One table, multiple readers.

    Storage simplicity. With Snowflake Storage for Iceberg (new, April 2026), you don’t manage S3 buckets yourself. Snowflake handles encryption, replication, Fail-safe recovery. You save the operational tax of bucket management, lifecycle policies, and debugging storage issues.

    So here’s the honest scorecard:

    For Snowflake-only users: Native tables win. Simpler, no migration pain, costs are identical.

    For multi-engine shops (Snowflake + Spark + Databricks): Iceberg wins. Egress fees alone justify the migration, and you get single source of truth as a bonus.

    The gotchas that will hurt your migration

    You can’t upgrade v2 tables to v3 in-place. There’s no ALTER TABLE ... SET ICEBERG_VERSION = 3. To get v3, you have to CREATE a new table. That means copying data (compute cost, time), repointing your queries, and hoping nothing breaks downstream. On large tables, this is a multi-day operation.

    External engines can’t write v3 tables yet. You can read v3 tables from Spark, Trino, DuckDB, all day. But writing is blocked. Snowflake says it’s “planned,” but if you’re building a shared Iceberg table that Spark needs to update, you’re stuck on v2. This is a major limitation if you’re counting on true multi-engine write access.

    Compaction gets billed starting May 21, 2026. When an external engine writes to an Iceberg table (via Spark, Trino, etc.), it creates small data files. Snowflake’s compaction automatically consolidates them into bigger files for query performance. Until May 21, that was free. Now it costs credits. Budget for ongoing compaction maintenance if you have heavy external write workloads.

    ⚠️ Don’t convert cloned tables with vended credentials. If you clone a native Snowflake table and then convert it to Iceberg, you can’t write to it with vended credentials (external query engine creds). You’d have to connect the external engine directly to your S3 bucket, defeating the whole point. Create the Iceberg table fresh if you’re using vended creds.

    Schema changes are cheap but metadata bloat is real. Iceberg tracks every schema change as a separate metadata version. On tables with thousands of ALTER COLUMN operations, metadata can get unwieldy. Compact your metadata regularly with CALL SYSTEM$OPTIMIZE(...).

    The mistakes teams make when migrating

    1. Migrating for the wrong reason. “Everyone’s talking about Iceberg, so we should move.” Wrong. Migrate only if you have a concrete use case: egress fees, multi-engine queries, or storage cost >$500/month. Otherwise you’re trading simplicity for nothing.

    2. Not testing external engine read performance first. Iceberg’s query performance depends heavily on your cloud setup, partitioning strategy, and how many small files are sitting around. Test Spark/Databricks queries on a small Iceberg table before migrating your 100 TB production table. You might find that your workload is slower on Iceberg, not faster.

    3. Assuming v3 is backward-compatible with v2. It’s not. Engines that only understand v2 (like older Spark runtimes, Trino versions) will fail on v3 tables. Check that every tool in your stack supports v3 *before* upgrading. v2 → v3 is one-way; there’s no downgrade.

    4. Ignoring the partition evolution story. Iceberg lets you change your partitioning scheme without rewriting the whole table. It’s a huge feature, but it’s also easy to mess up. Bad partitioning (e.g., partitioning by a column with 10 million distinct values) creates a partition explosion. Get your partitioning right before you migrate, not after.

    5. Migrating everything at once. Pick one critical table, migrate it, test multi-engine queries for a month, then move the rest. Iceberg is mature enough for production, but it’s not old enough that every edge case is documented. Be intentional.

    When to actually migrate: The real decision

    Stop and ask yourself: Do you actually need Iceberg?

    Yes, if: You query the same data from Snowflake and Spark/Databricks. You’re paying egress fees. You have data warehouses in multiple clouds and want to query across them. You’re building a data lakehouse and want to ditch proprietary formats.

    No, if: You only query from Snowflake. Your storage bill is <$500/month. You’re using Snowflake’s Time Travel, zero-copy clones, and other native features heavily. You don’t need to share data with other engines.

    For most teams, the answer is no. And that’s okay. Native Snowflake tables are extremely good. Simple, fast, well-integrated with dbt. There’s no shame in staying native.

    But for teams hitting the “data tax” — redundant copies, egress fees, multi-engine complexity — Iceberg v3 actually delivers. The gotchas are real, but they’re manageable. The cost savings are modest month-to-month, but the flexibility is transformative.

    The one principle that matters

    Interoperability beats simplicity when you’re already paying for fragmentation. If your current architecture already costs you $800/month in egress, $300/month in ETL pipelines, and engineering time chasing sync issues, Iceberg’s “complexity” is actually a simplification. You’re not adding complexity; you’re replacing it with a standard.

    If you’re simple and integrated today, stay there. Don’t pay the cost of flexibility you don’t need. But if you’re paying the data tax, Iceberg’s math changes fast.

    Related reading: Snowflake Apache Iceberg tables (official docs) · Snowflake Time Travel: The Real Architecture · Snowflake Optima: 15x Faster Queries at Zero Cost · Query Snowflake in DuckDB and Cut Costs

  • Snowflake Query Execution: what really happens under the hood

    Snowflake Query Execution: what really happens under the hood

    Ask ten data engineers what happens when you run a query in Snowflake and most of them will tell you the same thing: the warehouse runs it. SQL goes in, the warehouse chews on it, results come out. Clean, simple, and just wrong enough to cost you money. The truth is that Snowflake query execution is a trip through three separate layers, and the one you actually pay for is the last to get involved — if it gets involved at all.

    Flowchart of the life of a Snowflake query: query submitted, cloud services layer parses and prunes, result cache check returns a cached result on a hit with zero credits, and on a miss the virtual warehouse runs it across MPP nodes before fetching micro-partitions from storage and returning results.

    The path every query takes — and the shortcut a cache hit gets to skip compute entirely.

    TL;DR

    → Running a query isn’t one step. It’s a trip through three layers that scale independently, and the compute you pay for is the last one to wake up.

    → The cloud services layer does the thinking first — parses, plans, and prunes — using per-partition metadata to throw out data that can’t match your filter before a single byte is read.

    → If the exact same query ran in the last 24 hours and the data hasn’t moved, you get a cached result for free. No warehouse. No credits. Milliseconds.

    → Only on a cache miss does a virtual warehouse actually spin up and crunch the query across its nodes in parallel.

    → Warehouses keep recently-read micro-partitions on local SSD, but that cache vanishes the second the warehouse suspends.

    → “Identical” is brutally literal. A stray table alias, lowercase keywords, or a RANDOM() call quietly knocks you off the cache and back onto billed compute.

    → When a query is slow, it’s usually scanning too much, not computing too much. A bigger warehouse fixes the second problem and never the first.

    The mental model that’s quietly costing you money

    Here’s the reframe that changes how you think about every query: the virtual warehouse is the last thing to get involved, and plenty of queries never touch it at all. Almost everything that decides whether your query is fast or slow, cheap or expensive, happens in a layer you probably weren’t even picturing.

    Get this wrong and you end up doing what I’ve watched teams do a dozen times: a dashboard feels sluggish, someone bumps the warehouse from Medium to Large, it gets a little faster, everyone moves on. The bill goes up. The query was never compute-bound in the first place. They paid more to run the same broken query faster.

    So let’s actually follow a query, from the moment you hit run to the moment rows come back.

    The three layers a query passes through

    Snowflake splits into three layers, and the thing to internalize is that they scale completely independently. That’s not a trivia fact — it’s the reason the whole platform behaves the way it does. Every query touches all three. Some only touch the first.

    Diagram of Snowflake’s three layers: the cloud services layer handling parse, optimize, prune and result cache at the top; the compute layer with virtual warehouse MPP nodes in the middle; and the storage layer holding micro-partitions in S3, Azure Blob or GCS at the bottom, with a query entering the top and a cached result returning from it

    The cloud services layer is the brain. The warehouse is just the muscle.

    At the top sits the cloud services layer — the brain. Logins, sessions, parsing, query planning, transaction coordination, all the metadata: it lives here. Underneath that is the compute layer, made up of virtual warehouses. These are the MPP clusters that do the actual SQL grunt work. And at the bottom is storage: your data sitting in cloud object storage (S3, Azure Blob, or GCS) as immutable, compressed, columnar micro-partitions.

    The magic is that none of these share resources with each other. You can resize compute without moving a single byte of storage. You can point ten warehouses at the same table and none of them slow the others down. Keep that in your back pocket, because it’s exactly why the “just make the warehouse bigger” instinct fails so often.

    Step 1: The cloud services layer does the thinking

    Your query lands, and before any table data gets read, the cloud services layer goes to work.

    It parses the SQL, and the cost-based optimizer builds a plan. Then it does the single most important thing for performance, and it does it for free: partition pruning. Every micro-partition carries metadata, including the min and max value for each column. So when you write WHERE order_date = '2026-06-01', the optimizer reads that metadata first and skips every partition whose range can’t possibly hold a matching row. Ten thousand partitions in the table, three that actually get scanned. No index. No tuning. You didn’t do anything.

    This is also why a query can come back in milliseconds with no warehouse at all. Run a SELECT COUNT(*) or a MAX() and Snowflake often answers straight from the metadata cache — you’ll see a lone METADATA-BASED RESULT step in the query profile and zero compute on the bill.

    How good the pruning is comes down to how well your data is naturally ordered, which is the whole point of clustering and Snowflake’s automatic optimization features.

    Step 2: The result cache check, before any compute

    Now, still before any warehouse gets involved, cloud services checks the result cache. Identical query in the last 24 hours, data hasn’t changed since? Snowflake just hands back the stored result from the cloud services layer. Nothing resumes. Nothing gets billed. The answer is basically instant.

    This is the cheapest query you’ll ever run, and it’s the reason refreshing a dashboard a second time costs nothing. Here’s the part that surprises people: every time that cached result gets reused, the 24-hour clock resets. Keep hitting it and a result can stay alive for up to 31 days. A popular dashboard query can effectively sit in cache for a month and never cost a credit.

    Which brings us to the catch. “Identical” is doing a lot of work in that sentence, and it’s the thing nobody reads about until they’re staring at a bill wondering where the money went.

    Step 3: The virtual warehouse finally runs it

    Cache miss. Now — and only now — the query goes to a virtual warehouse. If it was asleep, it resumes (a second or two, usually). The warehouse is a little cluster of compute nodes, and this is where massively parallel processing earns its name: the surviving micro-partitions get split across the nodes and cores, and the filters, joins, and aggregations all happen in parallel.

    This is the layer with a meter running. You’re billed by the second, with a 60-second minimum every time a warehouse starts or resumes. That minimum trips people up constantly — a warehouse that keeps flickering on and off for tiny one-off queries can quietly cost more than one you just leave warm, because every single resume restarts that 60-second clock. It’s also the only layer where resizing actually helps, and that matters less often than you’d think.

    Step 4: Storage, and the cache that disappears

    The warehouse goes and fetches the micro-partitions that survived pruning. Each micro-partition holds 50–500 MB of uncompressed data in a columnar format, so only the columns you actually asked for get read. Ask for two columns out of fifty and you pay to read two.

    And there’s a second cache down here. As the warehouse pulls partitions from remote storage, it stashes them on local SSD. Next query on that same warehouse that needs the same data? It reads from SSD instead of making the round trip to object storage, which is a lot faster. This is what people mean when they say a warehouse “warms up.”

    The catch — and there’s always a catch — is that this cache gets wiped the instant the warehouse suspends. That’s the real trade-off hiding inside your AUTO_SUSPEND setting. Suspend fast and you stop paying for idle time but you toss the warm cache. Leave it running and you keep the cache but pay for the idle. A lot of teams land on 60 seconds, but honestly the right answer depends entirely on how often your queries actually fire.

    Three caches, one comparison to bookmark

    There are three caches, they live in different layers, and mixing them up is behind half the “wait, why was that slow?” conversations I’ve ever had.

    CacheLayerWhat it storesSurvives suspend?Compute cost
    Result cacheCloud servicesFinal query result setsYes (24h, resets on reuse, 31-day max)None
    Metadata cacheCloud servicesRow counts, min/max, distinct countsYesNone
    Local disk (warehouse) cacheComputeMicro-partitions read from storageNo — purged on suspendWarehouse already running

    The two cloud-services caches are shared across the whole account — every warehouse, every user benefits. The local disk cache belongs to one warehouse and dies with it. If you want the gory details with real query-profile screenshots, Snowflake’s own community piece on caching is the place to go.

    The cache rules nobody warns you about

    The result cache only kicks in when Snowflake decides the new query is identical to the old one, and “identical” is far more literal than anyone expects. Snowflake’s own docs spell it out: add a table alias, or just retype your keywords in lowercase, and you miss the cache. The query goes to billed compute and you never get a warning.

    It also bails on the cache if the query contains a non-deterministic function — UUID_STRING, RANDOM, RANDSTR are the usual suspects — or an external function, or if it reads from a hybrid table. And the role running it needs privileges on every table involved.

    ⚠️ What this means in practice: if you want your dashboards and scheduled jobs riding the cache, lock down the exact text your BI tool emits. Random aliasing, dynamic comments, an injected session variable, a stray current_timestamp() — any of it silently drops you off the cache, and you start paying to recompute answers you already had. The Query Cost Estimator is a quick way to put a number on what those misses are costing you.

    Why a bigger warehouse usually isn’t the answer

    This is the most expensive misunderstanding in all of Snowflake, so it’s worth being blunt about it. A query is slow for one of two reasons: it’s scanning too much data, or it’s doing too much actual computation. Resizing the warehouse only helps the second one.

    When the real problem is bad pruning — Snowflake had to scan most of the table because the data wasn’t laid out in a way it could skip — a bigger warehouse just reads the same mountain of data faster and charges you more per second for the privilege. The fix isn’t more nodes. It’s better pruning: clustering, a tighter filter, a smarter query shape. The query profile tells you which world you’re in. Look at partitions scanned versus partitions total. If you’re chewing through 95% of them, no warehouse size on earth is going to save you.

    And sometimes the honest answer to a giant, I/O-heavy scan isn’t a bigger Snowflake warehouse at all — it’s moving that work somewhere cheaper, which is the whole idea behind running the query in DuckDB instead. If you do decide a resize is warranted, the warehouse sizing guide will at least keep you from overshooting.

    What I actually check when a query misbehaves

    When something’s slow or weirdly expensive, here’s where I start. All of it comes out of the query profile and INFORMATION_SCHEMA — no guessing, no vibes.

    -- Did your last query hit the result cache?
    -- A cache hit shows bytes_scanned = 0 and a near-zero execution time.
    SELECT query_id,
           query_text,
           bytes_scanned,
           percentage_scanned_from_cache,
           execution_time / 1000 AS exec_seconds
    FROM TABLE(information_schema.query_history())
    ORDER BY start_time DESC
    LIMIT 10;
    
    -- Turn the result cache off for honest benchmarking (this session only)
    ALTER SESSION SET USE_CACHED_RESULT = FALSE;
    -- ...run your test queries...
    ALTER SESSION UNSET USE_CACHED_RESULT;
    
    -- Hunt down your worst pruning offenders
    SELECT query_id,
           partitions_scanned,
           partitions_total,
           ROUND(100 * partitions_scanned / NULLIF(partitions_total, 0), 1) AS pct_scanned,
           bytes_scanned / 1e9 AS gb_scanned
    FROM snowflake.account_usage.query_history
    WHERE partitions_total > 0
    ORDER BY bytes_scanned DESC
    LIMIT 20;
    
    -- Balance warm cache against idle cost
    ALTER WAREHOUSE analytics_wh SET AUTO_SUSPEND = 60;  -- seconds

    The number to stare at is pct_scanned in that third query. High on your slow queries? You’ve got a pruning problem, and that’s where your time should go — not the warehouse dropdown.

    What this actually costs you

    Architecture is interesting, but the bill is what makes it matter. So let’s put real numbers on it. Credit prices swing with your edition, cloud, and region, so treat these as illustrative — drop your own rate into the cost calculator for the exact figure.

    Picture one ordinary dashboard query. It runs on a Medium warehouse (4 credits an hour, call it roughly $3 a credit), and it fires 100 times a day because that’s how often people open the dashboard. Here’s how the same query plays out depending on whether the layers are doing their job:

    ScenarioWhat actually runs~Cost / day~Cost / month
    No cache, bad pruning (30s/run, scans ~95% of partitions)100 full compute runs~$10~$300
    Result cache hits 90% of the time10 compute runs, 90 free cached returns~$1~$30
    Cache + good pruning (3s/run, scans ~5%)10 runs at a tenth of the scan~$0.10~$3

    Same query. Same data. Roughly $300 a month versus $3, and the only difference is whether you let the result cache and pruning do what they’re built to do. Now multiply that across a dashboard with thirty queries on it, and you can see how a Snowflake bill quietly triples without anyone writing a single new query.

    The gotchas nobody warns you about

    The 60-second minimum punishes spiky workloads. A query that finishes in two seconds still bills a full minute if the warehouse had to resume to run it. Set AUTO_SUSPEND too aggressively on a workload that fires a query every couple of minutes and you’ll pay more in cold-start minimums than you ever saved on idle time. Aggressive suspend is not automatically cheaper.

    One write nukes the whole result cache. The cache is invalidated by any change to the underlying table, not just changes to the rows your query touched. A single late-arriving record at 2 a.m. quietly wipes the cached result for every dashboard query built on that table, and tomorrow morning they all run on compute again. If your “cached” dashboard mysteriously costs money some mornings, this is usually why.

    Cloud services is free, right up until it isn’t. The parsing, planning, and pruning in the cloud services layer is free — but only while it stays under 10% of your daily warehouse compute. Hammer it with thousands of tiny metadata queries or relentless INFORMATION_SCHEMA polling and you cross that line, and Snowflake starts billing the overage. Most teams never hit it; the ones running huge volumes of trivial queries do, and they never see it coming.

    The local cache doesn’t follow you between warehouses. That warm SSD cache belongs to one specific warehouse. Run your ETL on one warehouse and your reporting on another — sensible workload isolation — and the reporting warehouse gets exactly zero benefit from the partitions ETL just pulled. Each warehouse warms its own cache from cold.

    SELECT * throws away the columnar advantage. Snowflake only reads the columns you ask for. Ask for all of them and you pay to read all of them, even when the dashboard displays three. In a wide table that’s the difference between scanning a few columns and dragging the entire row off storage.

    The mistakes that quietly drain the budget

    Almost every overspending Snowflake account I’ve looked at is making some combination of these five:

    1. Leaving AUTO_SUSPEND at the 600-second default. Ten full minutes of paid idle after every burst of activity. On a warehouse that’s used in short bursts, that idle time can dwarf the actual query time. Most analytics warehouses are fine at 60 seconds.

    2. One giant warehouse for everything. Pile ETL, ad-hoc analysis, and dashboards onto a single warehouse and they fight over the cache and the compute. Separate warehouses per workload keep each cache warm and each workload predictable.

    3. Scaling up when the real fix is pruning. The reflex we opened with. If pct_scanned is high, a bigger warehouse just burns more credits reading the same data. Fix the layout, not the size.

    4. Letting the BI tool emit sloppy SQL. Inconsistent aliases, injected timestamps, and per-user comments mean “the same” dashboard query is never byte-for-byte identical, so it never reuses the result cache. Standardize what the tool sends.

    5. Over-clustering a hot table. Automatic clustering isn’t free — it spends credits reorganizing data in the background. On a table that’s written constantly, that background churn can cost more than the queries it speeds up. Cluster the tables you read far more than you write.

    The one principle to take away

    If you remember nothing else, remember this: scan less, don’t compute faster. Snowflake decides almost everything about a query’s speed and cost before the warehouse ever wakes up — in the pruning, in the caches, in how your data is laid out. The warehouse size is the last lever you should reach for, not the first. Get the layers above it working and most “we need a bigger warehouse” conversations simply stop happening.

    Related reading: Snowflake Time Travel: The Real Architecture · Snowflake Optima: 15x Faster Queries at Zero Cost · Query Snowflake in DuckDB and Cut Costs · Snowflake Streams & Tasks: SCD2 Pipeline Guide

  • How the Warehouse Cache Actually Works in Snowflake

    How the Warehouse Cache Actually Works in Snowflake

    A dashboard that ran in four seconds on Monday took nineteen seconds on Tuesday. Same query, same data, same warehouse size. I spent the better part of an hour convinced Snowflake was having a bad day, checking the status page, refreshing query history, muttering about “platform issues” in our team Slack — before I noticed our DevOps script had quietly added an aggressive auto-suspend policy the night before. The warehouse cache was getting wiped every single morning, and I’d built our entire “fast dashboard” reputation on a cache that reset itself before anyone showed up to work.

    TL;DR

    • Snowflake’s warehouse cache (local disk cache) stores raw compressed micro-partition data on each node’s SSD, not query results.
    • It’s separate from the result cache and metadata cache — three different caches, three different jobs.
    • Auto-suspending a warehouse wipes this cache completely. Resuming starts it cold every time.
    • You can verify it’s working with the ‘percentage scanned from cache’ field in query profile or ACCOUNT_USAGE.QUERY_HISTORY.
    • Multi-cluster warehouses don’t share this cache — a query routed to a new cluster starts cold even if a sibling cluster is warm.
    • You can’t manually size or pin it. The only real lever you control is the auto-suspend timer.

    That mistake is what got me actually reading how this thing works instead of just trusting that Snowflake would “handle it.” Turns out the warehouse cache isn’t magic, isn’t tunable, and isn’t the same thing as the result cache most people learn about first. Here’s what it actually is, where it lives, and how to tell when it’s helping you versus quietly costing you money.

    Diagram showing a cloud data warehouse architecture with three layers: Cloud services (result and metadata cache), compute layer with three SSD nodes, and a database storage layer with immutable micro-partitions. Arrows indicate data flow.

    Three caches, one name people use for all of them

    Snowflake has three distinct caching layers, and the confusion starts because people say “Snowflake caches my query” without specifying which one did the work. The result cache sits in the cloud services layer and stores entire finished query results — if the same exact SQL text runs again within 24 hours and the underlying data hasn’t changed, you get the answer back with bytes_scanned = 0 and zero compute cost. The metadata cache, also in cloud services, holds statistics about every micro-partition — min and max values per column, row counts — so Snowflake can decide which partitions to skip before it ever touches the data.

    The warehouse cache is the third one, and it’s the one this article is actually about. It lives on the local SSD of every node in a running virtual warehouse, and it stores raw, compressed micro-partition data — not query results, not aggregated answers, the actual columnar bytes that got pulled from remote storage to answer a scan.

    Why this distinction actually matters

    If you only know the result cache exists, you’ll misdiagnose a lot of performance issues. Change one character in a WHERE clause, add a comment, swap the role running the query — any of those bypass the result cache entirely, because it requires an exact text match. The warehouse cache doesn’t care about query text at all. It cares about which micro-partitions a query needs and whether those bytes are already sitting on a node’s SSD from a previous scan.

    What a micro-partition actually is

    You can’t understand the warehouse cache without understanding the unit it stores. When data lands in a Snowflake table, it gets automatically carved into micro-partitions — contiguous, immutable blocks holding somewhere between 50MB and 500MB of uncompressed data each, stored in a columnar format. There’s no manual partitioning scheme to design, no index to build. Snowflake just does this on every load.

    Each micro-partition carries its own metadata: minimum and maximum values for every column, which is exactly what the metadata cache is built from. When you filter a query on order_date > '2026-01-01', Snowflake checks that metadata first and skips any micro-partition whose max date falls before that threshold. That’s partition pruning, and it happens before a single byte gets pulled into the warehouse cache. Pruning decides what to read; the warehouse cache decides how fast a repeat read of the same partitions will be.

    The actual lifecycle of the warehouse cache

    Here’s the sequence that matters in practice. A warehouse resumes from suspended state with completely empty SSD — there’s nothing cached because the compute nodes assigned to it are freshly provisioned. The first query that touches a table has to pull every relevant micro-partition from remote cloud storage, which is the slowest tier in the whole architecture. As those partitions get read, they’re written to the local SSD cache as a side effect — not because you asked for caching, just because that’s what happens when a node reads remote data.

    The second query — if it touches the same micro-partitions and the warehouse is still running — can read those bytes off local SSD instead of going back to remote storage. This is meaningfully faster, and it’s why a sequence of similar queries against the same table speeds up the longer a warehouse stays warm. There’s no explicit “build the cache” step. Cache population is a byproduct of usage, which is exactly why a single cold query tells you almost nothing about real-world performance.

    Then the warehouse suspends, and it’s gone

    This is the part that bit me. When a warehouse auto-suspends, the compute nodes it was using get released back into Snowflake’s shared pool. The SSD on those specific nodes goes with them. When the warehouse resumes — even if it’s seconds later, even if it’s the exact same warehouse name — there’s no guarantee you get the same physical nodes back, and the cache starts from zero regardless. There’s no persistence, no “save state before suspending.” It’s just gone.

    Resizing a warehouse up or down does the same thing. A different size means a different set of nodes, which means different SSDs, which means the next queries run cold no matter how warm things were five minutes earlier.

    How to actually see this working

    Stop assuming and go look at it. Open the query profile for any query in Snowsight and check the IO statistics panel — there’s a field literally called percentage_scanned_from_cache. Run the same query twice in a row on a warm warehouse and watch that number jump from near 0% to something much higher on the second run. Suspend the warehouse, resume it, run the same query again, and watch it drop back to 0%. That’s the entire mechanism, visible in about ninety seconds of testing.

    For a wider view across your account, query history gives you the same field at scale. This is the query I run when someone asks “is our caching even helping”:

    Check your real cache hit rate (last 30 days, by warehouse)

    SELECT
        warehouse_name,
        COUNT(*) AS query_count,
        SUM(bytes_scanned) AS bytes_scanned,
        SUM(bytes_scanned * percentage_scanned_from_cache) AS bytes_from_cache,
        SUM(bytes_scanned * percentage_scanned_from_cache)
            / SUM(bytes_scanned) AS pct_scanned_from_cache
    FROM snowflake.account_usage.query_history
    WHERE start_time >= DATEADD(month, -1, CURRENT_TIMESTAMP())
      AND bytes_scanned > 0
    GROUP BY 1
    ORDER BY 5;

    A low percentage here on a warehouse running frequent, similar queries is a signal — either your auto-suspend timer is too aggressive for the workload, or the queries aren’t actually similar enough at the data level to benefit from a warm cache, even if the SQL looks similar to a human reading it.

    Result cache, warehouse cache, metadata cache — side by side

    Cache LayerWhere It LivesWhat It StoresCleared WhenCompute Cost
    Result CacheCloud Services layerFull query results24 hours of inactivity, or DDL on underlying tablesZero — no warehouse needed
    Warehouse CacheSSD on each compute nodeRaw compressed micro-partitionsWarehouse suspends, resizes, or node is replacedWarehouse must be running
    Metadata CacheCloud Services layerMin/max values, row counts, partition statsRarely — persists with the tableZero — used for pruning before scan

    The auto-suspend tradeoff nobody explains clearly

    Snowflake’s own guidance generally points toward short auto-suspend windows to control credit spend, and for spiky, unpredictable workloads that’s the right call. But if a warehouse runs frequent, similar queries back-to-back — a BI tool polling dashboards, an analyst iterating on the same fact table — an aggressive suspend timer means you’re paying the “cold scan” tax on nearly every query, because the cache never gets the chance to stay warm between them.

    The fix isn’t complicated once you see the tradeoff: separate warehouses by access pattern. A reporting warehouse that gets hit constantly during business hours can run a longer suspend window, or stay up during known peak hours, while a warehouse running sporadic ad-hoc analyst queries can suspend aggressively without losing much, since the cache wasn’t going to be useful between unrelated queries anyway.

    The multi-cluster gotcha

    If you’re running a multi-cluster warehouse for concurrency, know that clusters don’t share this cache with each other. Cluster A being fully warm doesn’t help a query that gets routed to Cluster B when Snowflake spins up a new cluster to handle a concurrency spike. That new cluster starts cold, scans from remote storage, and only builds its own local cache from that point forward. Teams chasing consistent query latency under high concurrency often get surprised by this — the warehouse “should” be warm, and on average it is, but any individual query can still land on a cold cluster.

    What the warehouse cache does not do

    It’s worth being precise about the boundaries here, because I’ve seen this cache get credited for things it isn’t responsible for. It doesn’t store query results — that’s the result cache’s job, and it’s a different layer entirely with a different lifetime. It doesn’t help with intermediate computation that spills to local disk during a large sort or hash join — that’s a separate spillage mechanism tracked under bytes spilled to local storage in query profile, not the same SSD allocation conceptually even though it physically lives in a similar place. And it provides no benefit on the write path for a fresh INSERT into new micro-partitions, since there’s nothing previously cached to reuse.

    What’s actually worth doing about this

    You don’t get a dial to resize this cache or pin specific tables into it, so the practical levers are all about behavior, not configuration. Match auto-suspend timers to actual access patterns instead of using one default across every warehouse. If a workload is genuinely cache-sensitive — recurring dashboards, iterative analyst sessions — consider a short warm-up query immediately after resume rather than letting the first real user query eat the cold-start cost. And when you’re debugging a “why did this get slower” ticket, percentage_scanned_from_cache should be one of the first three things you check, right alongside partition pruning stats, before you start blaming the query itself.

    For the deeper mechanics of how partition pruning interacts with clustering keys, the official Snowflake documentation on warehouse cache optimization is worth reading directly — it’s one of the rare vendor docs pages that actually shows the diagnostic query instead of just describing the concept. The micro-partitions and clustering documentation is the right follow-up if pruning efficiency turns out to be your actual bottleneck instead of cache temperature.

    Does the Snowflake warehouse cache get cleared when the warehouse suspends?

    Yes, completely. The warehouse cache lives on the SSD of the compute nodes assigned to that warehouse, and those nodes get released back to the pool on suspend. When the warehouse resumes — even seconds later — it’s starting from zero cached data.

    Why does percentage_scanned_from_cache show 0% on a brand new warehouse?

    Because there’s nothing to scan yet. The first query against any table after a cold start has to pull every micro-partition it needs from remote storage. Cache population happens as a side effect of running queries, not in advance.

    Does the warehouse cache help with INSERT, UPDATE, or DELETE performance?

    📷 the SSD cache layer, mid-rebuild after a warehouse resume — not glamorous, but it’s where the speed comes from

  • Everyone Said SQL Was Dead. It’s Now the Most Valuable Skill in AI (2026)

    Everyone Said SQL Was Dead. It’s Now the Most Valuable Skill in AI (2026)

    In 2018, a wave of Medium posts declared SQL obsolete. NoSQL was the future. Python would handle everything. Data lakes would make relational thinking irrelevant. The hot take had a good run.

    Then AI happened — and SQL came back harder than ever.

    Today, SQL is the connective tissue of every serious AI data stack. It feeds the training pipelines that power large language models. It validates the outputs of ML systems. It runs inside every dbt transformation, every Snowflake query, every Airflow DAG that touches structured data. And in 2026, the rise of text-to-SQL AI agents means that understanding SQL deeply is now more important than ever — not less.

    TL;DR

    For years, pundits called SQL a dying skill. They were wrong. In the AI era, SQL is experiencing a full renaissance — powering LLM pipelines, text-to-SQL agents, dbt models, and Snowflake-backed AI workflows. Senior data engineers with strong SQL command salaries up to $179K. Here’s why SQL is now the most career-defining skill in tech.

    Infographic with four stats: $179K senior data engineer max salary, 150K+ data engineering professionals, 20K+ new jobs created in past year, and 69% of job postings require SQL.

    The Death of SQL Was Always a Myth

    The “SQL is dying” narrative was never based on actual hiring data. It was based on hype cycles. Every new database technology generated thinkpieces about how SQL would be replaced — first by MapReduce, then document stores, then graph databases, then vector DBs.

    None of it displaced SQL as the default language of data work. And there’s a structural reason for that: relational thinking maps directly to how business data is structured. Revenue by region. Users by cohort. Transactions by date. These aren’t graph problems or document problems — they’re table problems, and SQL solves them with surgical precision.

    What the doomsayers missed is that SQL doesn’t compete with new technologies — it sits on top of them. Snowflake runs SQL. BigQuery runs SQL. Delta Lake and Apache Iceberg are queried with SQL. Even Snowflake’s AI features are invoked through SQL-adjacent interfaces.

    “SQL is eternal — it’s the new English of data systems.”

    Why AI Made SQL More Valuable, Not Less

    Here’s the counterintuitive reality: the rise of AI has created more demand for SQL, not less. There are three reasons why.

    1. LLMs Speak SQL

    The text-to-SQL category — where natural language queries get translated into executable SQL — is one of the fastest-growing areas in AI tooling. Tools like Vanna.ai, DataGrip’s AI Assistant, and BlazeSQL are putting SQL generation in the hands of non-technical users.

    But here’s the catch: AI-generated SQL still needs a human expert to validate it. A model hitting 80–85% accuracy on clean data sounds impressive until you realize that the 15% failure rate in production can silently corrupt dashboards, ML training sets, and financial reports. Someone with deep SQL knowledge has to own that validation layer.

    2. AI Models Are Trained on SQL Pipelines

    Every serious ML workflow has a data preparation layer. That layer runs on SQL. Whether it’s dbt transformations cleaning feature tables, Snowflake views materializing training datasets, or window functions creating temporal sequences for time-series models — SQL is the engine underneath.

    A data engineer who can write optimized SQL is not just a “database person.” They’re the person keeping AI models from training on garbage data. That’s a mission-critical role in 2026.

    3. The Semantic Layer Runs on SQL

    As AI agents get wired into data stacks, the “semantic layer” — a metadata-rich translation between business concepts and database schemas — has become critical infrastructure. dbt’s Semantic Layer, Snowflake’s Cortex, and tools like Cube.js all expose this layer through SQL-compatible interfaces. Understanding SQL deeply is what lets engineers build and maintain this layer correctly.

    The Modern SQL Skill Set Is Not What You Learned in 2015

    Basic SELECT * FROM table fluency is table stakes. What the market pays a premium for in 2026 is a completely different tier of SQL mastery.

    WITH user_activity AS (
      SELECT
        user_id,
        event_date,
        revenue,
        SUM(revenue) OVER (
          PARTITION BY user_id
          ORDER BY event_date
          ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
        ) AS rolling_7d_revenue,
        DATEDIFF('day', MAX(event_date) OVER(PARTITION BY user_id), CURRENT_DATE())
          AS days_since_last_event
      FROM events
      WHERE event_date >= DATEADD('day', -90, CURRENT_DATE())
    ),
    
    churn_signals AS (
      SELECT
        user_id,
        event_date,
        rolling_7d_revenue,
        days_since_last_event,
        -- Flag users with declining revenue trend
        CASE
          WHEN rolling_7d_revenue < LAG(rolling_7d_revenue, 7)
               OVER(PARTITION BY user_id ORDER BY event_date) * 0.7
          THEN 'HIGH_RISK'
          WHEN days_since_last_event > 14 THEN 'MEDIUM_RISK'
          ELSE 'LOW_RISK'
        END AS churn_risk
      FROM user_activity
    )
    
    SELECT * FROM churn_signals
    WHERE event_date = CURRENT_DATE() - 1
    ORDER BY rolling_7d_revenue DESC;

    This is what premium SQL work looks like in 2026: window functions generating ML features, CTEs composing complex business logic, and analytical patterns that feed directly into AI systems. It’s not query writing — it’s data architecture expressed in SQL.

    SQL vs. Python: The False Choice That Hurt Careers

    One of the most damaging career myths of the past decade was that SQL and Python were competing skills — as if choosing one meant abandoning the other. That binary thinking led many engineers to underinvest in SQL in favor of chasing Python frameworks, only to find that the highest-value data work required both.

    The truth is more nuanced. Python and SQL are complementary tools with clear division of labor in a modern data stack:

    TaskBest ToolWhy2026 Demand
    Data transformation at scaleSQL (via dbt)Declarative, version-controlled, warehouse-nativeVery High
    Feature engineering for MLSQL + PythonSQL for aggregations, Python for model inputsVery High
    Pipeline orchestrationPython (Airflow/Prefect)DAG logic, branching, retriesVery High
    Ad-hoc data explorationSQLFaster iteration, no environment setupHigh
    Real-time stream processingSQL (Flink/Kafka SQL)Streaming SQL increasingly the standardVery High
    Custom ML model trainingPythonscikit-learn, PyTorch, TensorFlowHigh
    Data quality & validationSQL (dbt tests)Schema-aware, automated, CI/CD-friendlyVery High
    Semantic layer / metricsSQL (dbt Semantic Layer)Business logic lives in SQL modelsEmerging

    What the Job Market Is Actually Saying

    Forget the hot takes. Look at the data. Across job postings, interview processes, and salary surveys, the signal is consistent: SQL is the single most requested skill in data roles, and that demand is accelerating.

    365 Data Science’s 2026 job outlook report found that 69.3% of data analyst postings explicitly require domain expertise that includes SQL as a core component. Data analyst average salaries have risen to $111,000 — up $20,000 from 2025 — driven largely by this demand.

    For data engineers — who live in SQL even more deeply — the numbers are stronger. Motion Recruitment’s 2026 salary guide puts senior data engineer salaries between $147,000 and $179,000. The data engineering sector now employs over 150,000 professionals with more than 20,000 new jobs created in the past year alone.

    “A senior engineer who writes clean, efficient SQL will always be more valuable than a junior who can only configure tools.”

    SQL in the AI-Native Stack: Where It Lives Now

    The modern data stack has evolved, but SQL is woven through every layer of it. Here’s where SQL shows up in a production AI workflow today:

    dbt: SQL as Software Engineering

    dbt (data build tool) transformed SQL from ad-hoc query language into version-controlled, testable, documented software. With the dbt Semantic Layer now powering AI applications directly, SQL models are becoming the canonical source of business logic across the entire organization. Following the Fivetran-dbt Labs merger, the tool’s dominance in the enterprise is only growing.

    Snowflake Cortex: AI Features in SQL

    Snowflake’s Cortex AI suite — rebranded and expanded after Summit 2026 — exposes large language model capabilities through SQL functions. You can run sentiment analysis, text classification, and vector search directly in SQL queries. Engineers who know SQL well have immediate access to AI capabilities without switching tools.

    Apache Flink & Kafka SQL: Streaming Goes SQL-First

    Even the streaming world is going SQL-native. Flink SQL and Kafka’s KSQL bring declarative query patterns to real-time data. As Apache Flink becomes the standard for event-driven AI applications, SQL fluency extends seamlessly from batch to streaming workloads.

    Vector Databases & Hybrid Search

    The newest frontier: hybrid SQL + vector search. Platforms like Snowflake, PostgreSQL with pgvector, and Databricks now support semantic similarity search alongside traditional SQL filtering. The engineers who can combine WHERE clauses with cosine similarity thresholds are building the retrieval layers that power RAG-based AI applications.

    Advanced SQL Concepts Every AI-Era Engineer Must Know

    Being competitive in 2026 means going well beyond JOINs and GROUP BYs. These are the SQL concepts that separate senior engineers from the rest:

    ConceptUse Case in AI WorkflowsDifficulty
    Window FunctionsTime-series feature engineering, rolling metricsIntermediate
    CTEs & Recursive CTEsHierarchical data modeling, lineage graphsIntermediate
    Query Execution PlansOptimizing training dataset queries at scaleIntermediate
    Lateral Joins / UNNESTFlattening JSON/semi-structured ML input dataIntermediate
    Incremental MaterializationEfficient dbt models on large datasetsAdvanced
    Partitioning & ClusteringCost-optimized queries on petabyte warehousesAdvanced
    Vector / Similarity Search SQLRAG retrieval layers, semantic search pipelinesAdvanced

    The Text-to-SQL Trap: Why AI Makes Human SQL Experts More Important

    There’s a seductive argument that text-to-SQL tools will eventually replace SQL expertise. It’s wrong, and understanding why matters for your career strategy.

    The best text-to-SQL tools in 2026 achieve 70–85% accuracy on clean, well-documented schemas. On messy enterprise databases with ambiguous column names and undocumented business logic, that number drops to 50–70%. Even with a proper semantic layer, you top out around 95%.

    That 5–30% failure rate is not a rounding error. It’s the difference between a business decision based on correct revenue data and one based on a silently wrong join. And crucially — AI cannot validate its own SQL output against business intent. A human who understands both the domain and the query language has to do that.

    The engineers who understand SQL deeply are not threatened by text-to-SQL. They’re empowered by it. They can build the semantic layers that make AI-generated queries more accurate, catch the failures that automated tools miss, and govern the data contracts that the entire stack depends on.

    How to Build SQL Mastery That Pays in 2026

    If you want to position yourself in the premium tier of data engineering talent, here’s a practical progression:

    Foundation (Weeks 1–4)

    Master complex multi-table JOINs, aggregations with GROUP BY and HAVING, and subqueries. Get comfortable with the full range of JOIN types and understand when to use each. Practice on real datasets — not toy examples.

    Intermediate (Months 2–3)

    Deep dive into window functions: ROW_NUMBER, RANK, LAG, LEAD, NTILE, and aggregate windows. Build comfort with CTEs for complex query decomposition. Start reading query execution plans in Snowflake or BigQuery.

    Advanced (Months 4–6)

    Learn how indexes and clustering keys affect performance at scale. Study how dbt compiles SQL and build production dbt models. Experiment with Snowflake Cortex SQL functions. Build a project that combines streaming SQL (Flink or Kafka SQL) with a batch warehouse layer.

    Expert (Ongoing)

    Build the semantic layer. Design data contracts. Validate AI-generated SQL. Architect the query patterns that power ML feature stores. At this level, SQL mastery translates directly into architecture decisions that affect every downstream system in the organization.

  • The Hidden Architecture Behind Snowflake Time Travel: Why It’s Not Really a Backup Feature

    The Hidden Architecture Behind Snowflake Time Travel: Why It’s Not Really a Backup Feature

    TL;DR

    → Time Travel is not a backup — it’s a versioned metadata pointer to immutable micro-partitions you already paid to store
    → Snowflake never overwrites data in place. Every UPDATE or DELETE creates new micro-partitions and marks old ones as expired
    → Standard edition gives you 1 day. Enterprise gives up to 90 days — but storage costs multiply fast on high-churn tables
    → After Time Travel expires, data moves to Fail-safe for 7 more days — but only Snowflake support can retrieve it
    → Zero-copy clones use the same micro-partition pointers — no extra storage until you diverge from the source
    → High-churn tables on 90-day retention can silently balloon your Snowflake bill by 10x


    The misconception that costs people money

    Most engineers who discover Time Travel think: “Great, we have backups.” That’s the wrong mental model — and it’s the one that leads to both security gaps and surprise storage bills. Time Travel is not a backup. It’s a metadata feature built on top of something Snowflake was already doing.

    Understanding why requires understanding how Snowflake actually stores data under the hood.

    How Snowflake stores data: micro-partitions

    Snowflake doesn’t store your tables as traditional database files. It stores them as micro-partitions — small, immutable, columnar files in cloud object storage (S3, Azure Blob, GCS), typically 50–500MB compressed each.

    The word immutable is the key. Snowflake never modifies a micro-partition once it’s written. Every micro-partition is a read-only snapshot of the data at the moment it was created. So what happens when you UPDATE a row? Snowflake writes a new micro-partition with the updated data and marks the old one as expired.

    Diagram showing data partitions before and after an update: before, partitions A and B are active; after, A is expired (for Time Travel), A* contains the updated row, and B remains active and unchanged.

    The old data doesn’t go anywhere immediately — it just gets a metadata flag saying ‘this version is no longer current.’ This is Copy-on-Write, and it’s the architectural foundation that makes Time Travel possible essentially for free.

    Time Travel isn’t a feature Snowflake built on top of backups. It’s a feature Snowflake built on top of an immutable storage model they were already using. The retained partitions are a side effect of how writes work — Time Travel just decides how long to keep them.

    What Time Travel actually is

    Time Travel is Snowflake’s metadata layer keeping pointers to those expired micro-partitions, instead of immediately flagging them for deletion. When you query with AT(TIMESTAMP => ...) or BEFORE, you’re not restoring from a backup. You’re asking Snowflake’s metadata layer to temporarily re-point to the expired partitions. The data was always there — you’re just re-routing the query to read older versions.

    This is why Time Travel queries are fast. There’s no restore process. No data movement. Snowflake reads directly from the older partitions.

    The three-zone model: Active, Time Travel, Fail-safe

    Understanding the full picture requires knowing all three zones data passes through after it’s written and then changed.

    A flowchart illustrates the three-zone data lifecycle: Active (current data, query anytime), Time Travel (1–90 days, billed, SQL access), Fail-safe (7 days, support only, Snowflake cost), and Gone (permanent, no recovery).

    Active data is what your current queries see — the live micro-partitions. Time Travel holds expired micro-partitions for your configured retention window. You can query this with SQL, clone from it, and UNDROP tables dropped within the window. Fail-safe activates when Time Travel expires — Snowflake keeps those partitions for 7 more days, but only Snowflake support can retrieve them. After that, data is permanently gone.

    Time Travel vs Fail-safe — the comparison you need

    FeatureTime TravelFail-safe
    Duration0–90 days (edition dependent)7 days (fixed, non-configurable)
    Who can accessYou — via SQL queriesSnowflake support only
    Query directlyYes — AT / BEFORE syntaxNo — support ticket required
    Clone fromYes — zero-copy clonesNo
    Storage costYes — counts against your billNo additional charge
    ConfigurableYes — per table/schema/databaseNo — always 7 days
    Best forOperational recovery, auditingLast-resort disaster recovery

    The storage cost nobody warns you about

    Every expired micro-partition kept for Time Travel counts against your Snowflake storage bill. The formula is brutal: a table with 90-day retention that sees 100% of its rows updated daily is storing 91 versions of itself simultaneously.

    Bar chart comparing storage multipliers for low churn (green) and high churn (orange) data over different retention periods (1, 7, 30, 90 days), showing high churn increases storage costs, especially at 90 days (30x).

    Most teams set 90-day retention on everything because the docs say Enterprise supports up to 90 days and more seems better. Then they get their first monthly storage invoice and start asking questions.

    ⚠️ The fix for high-churn tables: Set DATA_RETENTION_TIME_IN_DAYS = 0 on transient staging tables, session event tables, or any table where Time Travel has no operational value. You lose time travel on those tables, but you stop paying for micro-partitions you’ll never query.

    Zero-copy clones: same architecture, surprising implications

    Zero-copy clones work through the same micro-partition pointer mechanism. When you CREATE TABLE clone CLONE source, Snowflake doesn’t copy any data. It creates a new table object whose metadata points to the same micro-partitions as the source. Storage only diverges when you write new data to either the source or the clone.

    This is why ‘create a clone before a dangerous operation’ is nearly free — until you start modifying the clone. It’s also why clones on Time Travel windows are powerful: you can clone a table as it existed 7 days ago with zero storage cost at creation time.

    Why Time Travel is not a backup

    Account-level events affect everything. If your Snowflake account is compromised at the account level, or you accidentally drop the entire database, Time Travel data is in the same account. It’s not in a separate system.

    Cloud storage failure. Time Travel data lives in the same cloud storage as your active data. A regional disaster that takes out your Snowflake data takes out Time Travel with it.

    It expires. A backup you can restore from in 6 months is a backup. Time Travel data that’s gone after 90 days is a version history, not a backup. For genuine disaster recovery, you need cross-region replication or dedicated exports.

    Practical SQL patterns


    Here are the patterns I use most in production — from basic time travel queries to monitoring which tables are inflating your storage bill:

    -- Query a table as it existed yesterday
    SELECT * FROM orders
      AT(TIMESTAMP => DATEADD(DAY, -1, CURRENT_TIMESTAMP()));
    
    -- Query using a specific offset in seconds
    SELECT * FROM orders
      AT(OFFSET => -3600);  -- 1 hour ago
    
    -- Restore a dropped table
    UNDROP TABLE orders;
    
    -- Clone a table from 7 days ago (zero-copy, no extra storage)
    CREATE TABLE orders_snapshot
      CLONE orders
      AT(TIMESTAMP => DATEADD(DAY, -7, CURRENT_TIMESTAMP()));
    
    -- Check Time Travel storage usage by table
    SELECT table_name,
           active_bytes / 1e9         AS active_gb,
           time_travel_bytes / 1e9    AS time_travel_gb,
           failsafe_bytes / 1e9       AS failsafe_gb
    FROM information_schema.table_storage_metrics
    WHERE time_travel_bytes > 0
    ORDER BY time_travel_bytes DESC;
    
    -- Set retention to 0 for high-churn tables you don't need to travel
    ALTER TABLE session_events
      SET DATA_RETENTION_TIME_IN_DAYS = 0;

    Frequently Asked Questions

    Q: How does Snowflake Time Travel actually work?
    A: Time Travel works by retaining expired micro-partitions rather than deleting them. When you run UPDATE or DELETE, Snowflake writes new micro-partitions and marks old ones as expired but keeps them for your retention window. When you query with AT or BEFORE, Snowflake re-points to those expired partitions at the metadata level. No data is copied or moved — it’s a metadata operation.

    Q: Is Snowflake Time Travel the same as a backup?
    A: No. Time Travel is not a backup. It’s access to older versions of data in the same system. If your Snowflake account is deleted, compromised at account level, or if cloud storage fails, Time Travel disappears with it. For true disaster recovery you need cross-region replication or separate exports.

    Q: How long does Snowflake Time Travel last?
    A: Standard edition: maximum 1 day. Enterprise and higher: up to 90 days, configurable per table, schema, or database. Transient and temporary tables max out at 1 day regardless of edition.

    Q: What happens after Time Travel expires?
    A: Expired micro-partitions move to Fail-safe — a non-configurable 7-day window managed by Snowflake. You cannot query Fail-safe data yourself. Only Snowflake support can recover it, and recovery is not guaranteed. After Fail-safe expires, data is permanently deleted.

    Q: Does Time Travel affect storage costs?
    A: Yes, significantly. Every expired micro-partition counts toward your storage bill. High-churn tables on 90-day retention can cost 10x more storage than the active data alone. Set DATA_RETENTION_TIME_IN_DAYS = 0 on staging tables or high-churn tables where Time Travel has no operational value.

    Q: What’s the difference between Time Travel and Fail-safe?
    A: Time Travel is user-controlled — you query it with SQL, configure its duration, and clone from it. Fail-safe is Snowflake-controlled — only support can access it, it’s always exactly 7 days, and it exists for Snowflake’s disaster recovery, not yours.

  • Snowflake CoCo Desktop — What It Is, How It Works, and Whether It’s Worth It

    Snowflake CoCo Desktop — What It Is, How It Works, and Whether It’s Worth It

    Snowflake just announced a lot at Summit 2026. Most of it was the usual conference noise. CoCo Desktop isn’t.

    I’ve been following Cortex Code — now officially rebranded as CoCo — since it first shipped in Snowsight. The Summit 2026 announcement on June 2 changed the scope significantly. A native desktop IDE, Cloud Agents that run async without keeping your machine on, a Slackbot, mobile app, and integrations with VS Code, Excel, and Claude Code. That’s not a feature update. That’s a platform play.

    Here’s my honest take on what CoCo Desktop actually is, how it compares to what you’re probably already using, and whether data engineers should care.


    TL;DR

    → Snowflake CoCo is the official rebrand of Cortex Code — same product, bigger vision, launched at Summit 2026 on June 2
    → CoCo Desktop is a native IDE that reads your Snowflake schemas, RBAC policies, and lineage before generating any code
    → It scored 72.1% on dbt’s ADE-Bench vs 65.1% for Claude Code — but benchmarks and production are different things
    → New at Summit: Cloud Agents run tasks async in Snowflake’s cloud, Automations handle recurring workflows, Skill Catalog shares reusable flows
    → It integrates with VS Code, Slack, Excel, and Claude Code — so you don’t have to abandon your existing tools
    → Worth evaluating if your team lives in Snowflake — not worth migrating to if you’re happy with Claude Code or Cursor


    What CoCo Actually Is (And What Changed at Summit 2026)

    CoCo (formerly Cortex Code) is Snowflake’s data-native AI coding agent. The key word is data-native. Unlike general-purpose coding assistants, CoCo reads your live Snowflake environment — schemas, RBAC policies, lineage — before generating anything. It doesn’t generate SQL and hope it matches your tables. It knows your tables.

    That’s been true since Cortex Code. What changed at Summit 2026:

    CoCo Desktop — a native desktop IDE, not just a Snowsight panel. Full agentic development, local environment, MCP integrations.

    Cloud Agents — launch tasks that run async in Snowflake’s cloud. Your machine doesn’t need to stay on. This is the biggest shift.

    Automations — recurring, event-driven workflows. CoCo monitors, validates, and triggers without manual intervention.

    Skill Catalog — teams can share and reuse proven CoCo workflows. Institutional knowledge, codified.

    The rebranding from Cortex Code to CoCo also signals something: Snowflake is positioning this as a platform, not a feature. The Cortex branding was burying it inside a product umbrella. CoCo gets its own identity, its own downloads page, and its own development roadmap.


    The Data-Native Context Advantage

    This is where CoCo genuinely differentiates from Claude Code, Cursor, or GitHub Copilot.

    Every general-purpose coding assistant has the same fundamental problem with data engineering work: it doesn’t know your data. You ask it to write a dbt model and it writes syntactically valid SQL against tables that may or may not exist in your warehouse. You have to paste schema context, explain relationships, describe RBAC constraints. It’s friction.

    CoCo eliminates that friction for Snowflake environments. Before generating anything, it reads your live schema, understands which tables your role can access, and traces lineage to understand dependencies. The output is grounded in your actual environment.

    Here’s a real example of the difference — a natural language prompt to CoCo and what it generates knowing your schema:

    CoCo Desktop — Schema-Grounded dbt Model Generation

    -- Example: What you say to CoCo Desktop
    "Create an incremental dbt model that calculates 
    7-day rolling revenue by customer segment, 
    partitioned by signup_date"
    
    -- What CoCo generates (reads your actual schema first):
    {{ config(
        materialized='incremental',
        unique_key='customer_segment || date_trunc(day, event_date)',
        on_schema_change='sync_all_columns'
    ) }}
    
    WITH revenue_base AS (
        SELECT
            c.segment                          AS customer_segment,
            DATE_TRUNC('DAY', o.created_at)    AS event_date,
            SUM(o.revenue_usd)                 AS daily_revenue
        FROM {{ ref('orders') }} o
        JOIN {{ ref('customers') }} c ON o.customer_id = c.id
        {% if is_incremental() %}
        WHERE o.created_at >= (
            SELECT DATEADD(DAY, -8, MAX(event_date)) FROM {{ this }}
        )
        {% endif %}
        GROUP BY 1, 2
    )
    SELECT
        customer_segment,
        event_date,
        daily_revenue,
        AVG(daily_revenue) OVER (
            PARTITION BY customer_segment
            ORDER BY event_date
            ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
        ) AS rolling_7d_revenue
    FROM revenue_base

    The output references your actual table names, your actual columns, and respects your actual partitioning strategy — because CoCo read your schema before writing a single line.

    Cloud Agents — Async Scheduled Tasks

    The second genuinely new capability is Cloud Agents. Here’s what that looks like in practice:

    -- Cloud Agent example: schedule a validation job
    -- that runs without keeping your laptop open
    
    -- In CoCo Desktop → New Cloud Agent:
    {
      "name": "daily_revenue_validation",
      "trigger": "schedule",
      "cron": "0 6 * * *",
      "task": "Run data quality checks on orders table,
               flag anomalies > 2 std deviations,
               post summary to #data-alerts Slack channel",
      "context": ["orders", "customers", "revenue_daily"],
      "on_failure": "notify_slack"
    }
    
    -- CoCo generates, schedules, and monitors this
    -- entirely within Snowflake's governed environment

    This is the shift from coding assistant to autonomous agent. CoCo doesn’t just help you write the job — it runs the job, in Snowflake’s governed environment, on a schedule, and reports back.


    The Benchmark Reality Check

    Snowflake claims CoCo scored 72.1% on dbt’s ADE-Bench versus 65.1% for Claude Code. That’s a real benchmark on real analytics engineering tasks — 145 queries, statistically significant.

    I want to be honest about what this means and doesn’t mean.

    It means CoCo is genuinely better at Snowflake-specific SQL and dbt model generation than Claude Code in a controlled evaluation. That’s not surprising — CoCo has live schema context and was purpose-built for this use case.

    It doesn’t mean CoCo is better for all the work you actually do. ADE-Bench measures analytics engineering tasks specifically. It doesn’t measure debugging Python pipeline errors, writing Airflow DAGs, reviewing infrastructure-as-code, or any of the other things Claude Code or Cursor handle in a typical data engineering workday.

    If 80% of your coding work is Snowflake SQL and dbt models, CoCo’s benchmark advantage is real and production-relevant. If you’re a generalist data engineer working across multiple systems, that 7-point advantage on analytics SQL is a smaller part of your actual workflow.


    Where CoCo Desktop Has Limits

    No offline mode. CoCo Desktop requires a Snowflake account connection. If you’re working without internet access or in an environment where outbound connections are restricted, it doesn’t work.

    Snowflake-only context. CoCo understands your Snowflake environment deeply. It doesn’t understand your Postgres database, your Kafka topics, or your Airflow DAG structure unless you give it that context manually — at which point you’ve lost the data-native advantage.

    Token-based pricing. Cloud Agents and Automations consume Snowflake credits. For high-frequency automation workflows, the cost model needs evaluation before you commit. This is a brand new product — pricing behaviour at scale is unknown.

    MCP ecosystem is smaller than Claude Code’s. CoCo supports GitHub, Jira, Google Workspace via MCP. Claude Code’s MCP ecosystem is broader. If your workflow relies on specific MCP integrations, check the current list before assuming coverage.


    The Comparison You Actually Need

    FeatureCoCo DesktopClaude Code / Cursor
    Data contextReads live Snowflake schema, RBAC, lineage automaticallyNo native warehouse context — you provide manually
    SQL generation72.1% ADE-Bench — purpose-built for analytics SQL65.1% ADE-Bench — strong general coding
    dbt supportNative — reads dbt project structure and modelsGood — but no automatic schema grounding
    Pipeline authoringSnowflake-native — Snowpark, Streams, TasksGeneral Python — works but no Snowflake operators
    Cloud AgentsRun tasks async in Snowflake cloudLocal execution only
    MCP integrationsGitHub, Jira, Google WorkspaceBroader third-party connector ecosystem
    Slack / mobileSlackbot and mobile app coming soonNo native Slack or mobile interface
    GovernanceRBAC-aware — won’t violate access policiesNo governance layer — manual enforcement
    Best forTeams fully on SnowflakeGeneral data engineering, polyglot stacks

    How I’d Actually Use This

    I wouldn’t replace Claude Code with CoCo. I’d use them for different things.

    CoCo Desktop for: writing dbt models, generating Snowpark pipelines, setting up Cloud Agents for recurring validation jobs, anything where Snowflake schema context is the difference between useful output and generic SQL.

    Claude Code for: debugging Python pipeline errors, writing Airflow DAGs, reviewing infrastructure code, cross-system work, anything outside the Snowflake context boundary.

    The Skill Catalog is the feature I’m most interested in practically. Codifying proven CoCo workflows — a data quality check pattern, a standard incremental model template, a Snowflake Stream processing pattern — and sharing them across the team is where the real leverage is. That’s institutional knowledge made reusable. I wrote about a similar pattern in Delta Lake vs Iceberg — the tools that win long-term are the ones that compound team knowledge, not just individual productivity.


    When to Evaluate CoCo Desktop

    Your team is primarily on Snowflake. If 70%+ of your data work is in Snowflake, CoCo’s context advantage is real and compounding. The time saved not pasting schema context into Claude Code adds up fast.

    You need governed AI development. CoCo’s RBAC awareness means it won’t generate queries that violate access policies. For compliance-heavy environments, that’s not a nice-to-have.

    You want async agentic workflows. Cloud Agents are genuinely new. If you want to describe a monitoring job in natural language and have it run on a schedule without babysitting it, CoCo is currently the only tool that does this inside a governed Snowflake environment.

    When to Stick With What You Have

    You’re on a polyglot stack. Snowflake is one of several systems. CoCo’s advantage disappears outside the Snowflake context boundary.

    You’re happy with Claude Code or Cursor. The 7-point ADE-Bench gap doesn’t justify a tool switch if your current workflow is working and your team is productive.

    You want to wait for GA. CoCo Desktop is very new — announced June 2, 2026. Production edge cases, pricing at scale, and Cloud Agent reliability are unknown quantities. Evaluating in staging is smart. Full production adoption before GA carries risk.


    What I’d Do Right Now

    Download CoCo Desktop and run it against one real project — ideally a dbt model you’ve been meaning to refactor or a validation job you’ve been doing manually. That’s the fastest way to evaluate whether the schema-grounding advantage is worth it for your specific workflow.

    Don’t make a team-wide decision based on benchmarks alone. ADE-Bench is a good signal, but your specific schema complexity, RBAC structure, and workflow patterns will determine whether the 7-point advantage is meaningful in practice.

    Watch the Cloud Agents closely. That’s where the real competitive moat is if Snowflake executes. An AI agent that runs governed, async, schema-aware tasks without manual intervention is a different category from a coding assistant.


    Frequently Asked Questions

    Q: What is Snowflake CoCo Desktop?
    A: CoCo Desktop is a native desktop IDE from Snowflake that connects directly to your Snowflake account and uses AI to generate SQL, dbt models, and pipelines from natural language. It reads your live schema, RBAC policies, and data lineage before generating any code — meaning it understands your actual data environment, not just generic SQL syntax. It was announced at Snowflake Summit 2026 on June 2 as the rebrand of Cortex Code.

    Q: Is Snowflake CoCo the same as Cortex Code?
    A: Yes. CoCo is the official rebrand of Cortex Code, announced at Snowflake Summit 2026. The product functionality and architecture are the same — the rename reflects Snowflake’s broader vision for AI-powered development. If you were using Cortex Code, nothing changes in your existing workflows.

    Q: How does CoCo Desktop compare to Claude Code?
    A: CoCo scored 72.1% on dbt’s ADE-Bench versus 65.1% for Claude Code on analytics engineering tasks — but the more important difference is context. CoCo reads your Snowflake schema, RBAC, and lineage automatically. Claude Code needs you to provide that context manually. For teams fully on Snowflake, CoCo’s data-native context is a real advantage. For polyglot stacks or non-Snowflake work, Claude Code is still stronger.

    Q: What are CoCo Cloud Agents?
    A: Cloud Agents let you launch tasks in Snowsight that run async in Snowflake’s cloud — without your laptop staying open. You describe the task in natural language, CoCo generates and schedules it, and it runs in a governed Snowflake environment. This is the key difference from a coding assistant — Cloud Agents turn CoCo into an autonomous development platform, not just an autocomplete tool.

    Q: What tools does CoCo Desktop integrate with?
    A: CoCo integrates with VS Code, Slack (Slackbot, with mobile app coming soon), Microsoft Excel, and Anthropic’s Claude Code via MCP. It also supports MCP servers for GitHub, Jira, and Google Workspace. You don’t have to abandon your existing tools — CoCo is designed to work alongside them.

    Q: Is CoCo Desktop free?
    A: CoCo Desktop requires a Snowflake account with Cortex Code enabled and is billed based on token consumption. Snowflake offers trial access with free credits for new users. Costs depend on query volume and token usage — check Snowflake’s pricing page for the latest details since this launched at Summit 2026 in June.