The Night Everything Broke (And How Streams Saved Me)
It was 2 AM on a Tuesday. My phone was buzzing non-stop. Our nightly ETL job had failed—again. This time, it crashed after processing 6 hours of data, and we had to start over from scratch. The business needed fresh customer data by 8 AM for the morning reports.
I was running a classic batch process: every night at midnight, truncate the target table, reload everything from source, rebuild all the aggregations. It worked fine when we had 100,000 customers. But we’d grown to 5 million customers, and the full reload was taking 8 hours.
That’s when I discovered Streams and Tasks in Snowflake. Within a week, I rebuilt the entire pipeline:
- No more full reloads (only process changes)
- No more manual scheduling (Tasks handled it)
- No more 8-hour batch windows (incremental updates took 10 minutes)
- No more 2 AM phone calls (built-in retry logic)
This guide is everything I wish someone had shown me that night. We’ll build real pipelines—not toy examples—starting from simple automation and working up to complex patterns like SCD Type 2.
What Are Streams and Tasks? (The Simple Explanation)
Before diving into code, let’s understand what these actually do:
Streams are like security cameras for your tables. They watch what changes (inserts, updates, deletes) and create a “change log” you can query. Think of it as automatic change data capture (CDC) built into Snowflake.
Tasks are scheduled SQL jobs. They’re like cron jobs but smarter—they can depend on other tasks, run only when data is available, and auto-retry on failure.
Together? Magic. Streams detect changes, Tasks process them automatically.
The old way:
SQL example — read the query, then copy it into your warehouse.
-- Run this manually or via cron at 2 AMTRUNCATE TABLE customer_summary;INSERT INTO customer_summary SELECT customer_id, COUNT(*) as order_count, SUM(amount) as total_spentFROM ordersGROUP BY customer_id;-- Takes hours, processes everything, fails if interrupted
The new way:
SQL example — read the query, then copy it into your warehouse.
-- Stream watches for changesCREATE STREAM order_changes ON TABLE orders;-- Task processes only changes, runs automaticallyCREATE TASK update_customer_summary SCHEDULE = '5 MINUTE' WHEN SYSTEM$STREAM_HAS_DATA('order_changes')AS -- Process only changed orders (10 seconds instead of 8 hours!) MERGE INTO customer_summary ...
Let’s build this properly.
Part 1: Understanding Streams (Change Data Capture)
Creating Your First Stream
-- Setup: Create sample source table
CREATE OR REPLACE TABLE customers (
customer_id INTEGER,
customer_name STRING,
email STRING,
status STRING,
created_date DATE,
updated_at TIMESTAMP_LTZ DEFAULT CURRENT_TIMESTAMP()
…The remaining 11 lines stay in the interactive article so this page remains a written walkthrough rather than a raw SQL dump.
How Streams Capture Changes
Now let’s make some changes and see what the stream captures:
-- Make various changes
UPDATE customers SET status = 'INACTIVE' WHERE customer_id = 1;
INSERT INTO customers VALUES (4, 'Alice Williams', '[email protected]', 'ACTIVE', '2024-04-05', CURRENT_TIMESTAMP());
DELETE FROM customers WHERE customer_id = 3;
-- Query the stream
SELECT
customer_id,
customer_name,
…The remaining 6 lines stay in the interactive article so this page remains a written walkthrough rather than a raw SQL dump.
Output:
SQL example — read the query, then copy it into your warehouse.
customer_id | customer_name | status | METADATA$ACTION | METADATA$ISUPDATE1 | John Doe | INACTIVE | INSERT | TRUE1 | John Doe | ACTIVE | DELETE | TRUE4 | Alice Williams | ACTIVE | INSERT | FALSE3 | Bob Johnson | ACTIVE | DELETE | FALSE
Understanding the output:
- UPDATE appears as DELETE (old value) + INSERT (new value) with
METADATA$ISUPDATE = TRUE - INSERT appears as INSERT with
METADATA$ISUPDATE = FALSE - DELETE appears as DELETE with
METADATA$ISUPDATE = FALSE
Stream Consumption (Critical Concept)
Here’s something that confused me for weeks: streams are consumed when you read from them in a DML operation.
SQL example — read the query, then copy it into your warehouse.
-- Query stream (doesn't consume)SELECT * FROM customer_changes;-- Stream still has data-- Use stream in INSERT (consumes!)INSERT INTO customer_backupSELECT * FROM customer_changes;-- Query stream againSELECT * FROM customer_changes;-- Returns 0 rows - stream was consumed!
Important patterns:
SQL example — read the query, then copy it into your warehouse.
-- If you need the data multiple times, capture it firstCREATE TEMPORARY TABLE changes_snapshot ASSELECT * FROM customer_changes;-- Now use snapshot multiple timesINSERT INTO target1 SELECT * FROM changes_snapshot;INSERT INTO target2 SELECT * FROM changes_snapshot;-- Stream is only consumed once
Part 2: Understanding Tasks (Automation)
Creating Your First Task
-- Simple task that runs on schedule
CREATE OR REPLACE TASK hello_world_task
WAREHOUSE = my_wh
SCHEDULE = '5 MINUTE'
AS
INSERT INTO task_logs
VALUES ('Hello from task!', CURRENT_TIMESTAMP());
-- Tasks are created in SUSPENDED state
…The remaining 15 lines stay in the interactive article so this page remains a written walkthrough rather than a raw SQL dump.
Conditional Execution (Run Only When Needed)
-- Create task that only runs when stream has data
CREATE OR REPLACE TASK process_customer_changes
WAREHOUSE = etl_wh
SCHEDULE = '5 MINUTE'
WHEN SYSTEM$STREAM_HAS_DATA('customer_changes')
AS
INSERT INTO customer_history
SELECT
…The remaining 8 lines stay in the interactive article so this page remains a written walkthrough rather than a raw SQL dump.
Why this is powerful:
- Task checks every 5 minutes
- Only runs if stream has data
- Warehouse only spins up when needed
- Zero cost if no changes
Task Dependencies (Building Pipelines)
-- Create a pipeline: raw → staging → production
-- Task 1: Load raw data
CREATE OR REPLACE TASK load_raw_data
WAREHOUSE = etl_wh
SCHEDULE = '10 MINUTE'
AS
COPY INTO raw_orders
FROM @s3_stage/orders/
…The remaining 41 lines stay in the interactive article so this page remains a written walkthrough rather than a raw SQL dump.
Task dependency diagram:
Code example — copy the snippet, then match it to your project.
load_raw_data (every 10 min) ↓stage_data (after load_raw_data completes) ↓aggregate_to_prod (after stage_data completes)
Part 3: Real Use Case #1 – Simple Incremental Load
Scenario: Load customer orders from source system, keep only net changes.
-- Source table (simulates external system)
CREATE OR REPLACE TABLE source_orders (
order_id INTEGER,
customer_id INTEGER,
product_name STRING,
amount DECIMAL(10,2),
order_date DATE,
updated_at TIMESTAMP_LTZ DEFAULT CURRENT_TIMESTAMP()
…The remaining 54 lines stay in the interactive article so this page remains a written walkthrough rather than a raw SQL dump.
Why this works:
- Stream captures all changes
- Task runs only when changes exist
- MERGE handles both new and updated records
- Fully automated, zero manual intervention
Part 4: Real Use Case #2 – SCD Type 2 (The Big One)
This is the use case everyone asks about. Slowly Changing Dimensions Type 2 tracks full history of changes.
Business requirement: Track complete history of customer data changes over time.
Step 1: Create SCD2 Table Structure
-- Dimension table with SCD2 pattern
CREATE OR REPLACE TABLE dim_customer_scd2 (
customer_key INTEGER AUTOINCREMENT, -- Surrogate key
customer_id INTEGER, -- Natural key
customer_name STRING,
email STRING,
phone STRING,
address STRING,
…The remaining 44 lines stay in the interactive article so this page remains a written walkthrough rather than a raw SQL dump.
Step 2: Create Stream on Source
SQL example — read the query, then copy it into your warehouse.
CREATE OR REPLACE STREAM source_customers_stream ON TABLE source_customers;
Step 3: Build SCD2 Processing Logic
This is the complex part. We need to:
- Identify what changed
- Expire old records (set end date, is_current = FALSE)
- Insert new versions
CREATE OR REPLACE TASK process_customer_scd2
WAREHOUSE = etl_wh
SCHEDULE = '10 MINUTE'
WHEN SYSTEM$STREAM_HAS_DATA('source_customers_stream')
AS
BEGIN
-- Step 1: Expire old records for changed customers
UPDATE dim_customer_scd2
…The remaining 52 lines stay in the interactive article so this page remains a written walkthrough rather than a raw SQL dump.
Step 4: Test SCD2 Processing
-- Test 1: Update customer address (should create new version)
UPDATE source_customers
SET address = '999 New Street', city = 'San Francisco', state = 'CA'
WHERE customer_id = 1;
-- Wait 10 minutes (or manually run: EXECUTE TASK process_customer_scd2;)
-- Check results - should see 2 versions of customer 1
SELECT
customer_key,
…The remaining 15 lines stay in the interactive article so this page remains a written walkthrough rather than a raw SQL dump.
-- Test 2: Update multiple attributes
UPDATE source_customers
SET
email = '[email protected]',
phone = '555-9999',
status = 'INACTIVE'
WHERE customer_id = 2;
-- Check history
…The remaining 17 lines stay in the interactive article so this page remains a written walkthrough rather than a raw SQL dump.
Step 5: Query Historical Data
Now the payoff—querying data as it was at any point in time:
-- Current state (easy)
SELECT * FROM dim_customer_scd2 WHERE is_current = TRUE;
-- State as of specific date
SELECT
customer_id,
customer_name,
address,
city,
…The remaining 30 lines stay in the interactive article so this page remains a written walkthrough rather than a raw SQL dump.
Part 5: Real Use Case #3 – Multi-Table Pipeline
Scenario: Process orders → update customer metrics → trigger alerts
-- Source tables
CREATE OR REPLACE TABLE raw_orders (
order_id INTEGER,
customer_id INTEGER,
amount DECIMAL(10,2),
order_date DATE,
status STRING,
inserted_at TIMESTAMP_LTZ DEFAULT CURRENT_TIMESTAMP()
…The remaining 90 lines stay in the interactive article so this page remains a written walkthrough rather than a raw SQL dump.
Part 6: Error Handling and Monitoring
Handling Task Failures
-- Task with error handling
CREATE OR REPLACE TASK robust_processing
WAREHOUSE = etl_wh
SCHEDULE = '10 MINUTE'
AS
BEGIN
-- Use TRY-CATCH pattern
INSERT INTO processing_log VALUES ('Starting process', CURRENT_TIMESTAMP(), NULL);
…The remaining 12 lines stay in the interactive article so this page remains a written walkthrough rather than a raw SQL dump.
Monitoring Task Execution
-- Create monitoring view
CREATE OR REPLACE VIEW task_monitoring AS
SELECT
name as task_name,
state,
schedule,
warehouse_name,
error_code,
…The remaining 25 lines stay in the interactive article so this page remains a written walkthrough rather than a raw SQL dump.
Stream Lag Monitoring
-- Check if streams are falling behind
SELECT
table_name,
stream_name,
SYSTEM$STREAM_GET_TABLE_TIMESTAMP(stream_name) as stream_position,
CURRENT_TIMESTAMP() as current_time,
DATEDIFF(minute,
SYSTEM$STREAM_GET_TABLE_TIMESTAMP(stream_name),
…The remaining 19 lines stay in the interactive article so this page remains a written walkthrough rather than a raw SQL dump.
Part 7: Performance Optimization
Tip 1: Minimize Stream Scans
-- Bad: Scanning stream multiple times
CREATE TASK inefficient_task AS
BEGIN
INSERT INTO table1 SELECT * FROM my_stream WHERE condition1;
INSERT INTO table2 SELECT * FROM my_stream WHERE condition2;
END;
-- Problem: Stream scanned twice (expensive!)
-- Good: Scan once, use temp table
…The remaining 9 lines stay in the interactive article so this page remains a written walkthrough rather than a raw SQL dump.
Tip 2: Use Clustering for Large Streams
Code example — copy the snippet, then match it to your project.
-- If stream source table is large, cluster itALTER TABLE large_source_table CLUSTER BY (date_column);-- Improves stream performance significantly
Tip 3: Right-Size Warehouses
SQL example — read the query, then copy it into your warehouse.
-- Use smaller warehouses for simple tasksCREATE TASK simple_aggregation WAREHOUSE = X_SMALL_WH -- Don't waste credits SCHEDULE = '5 MINUTE'AS ...;-- Use larger for complex processingCREATE TASK complex_transformations WAREHOUSE = LARGE_WH SCHEDULE = '1 HOUR'AS ...;
Tip 4: Task Scheduling Strategy
SQL example — read the query, then copy it into your warehouse.
-- Stagger tasks to avoid warehouse contentionCREATE TASK task_1 SCHEDULE = 'USING CRON 0 * * * * UTC' AS ...; -- Every hour at :00CREATE TASK task_2 SCHEDULE = 'USING CRON 15 * * *
Question this article answers
The short answer first. Open it to read the working note.
What Are Streams and Tasks? (The Simple Explanation)?
Before diving into code, let's understand what these actually do: Streams are like security cameras for your tables. They watch what changes (inserts, updates, deletes) and create a "change log" you can query. Think of it as automatic change data capture (CDC) built into Snowflake.
