Modern Data Warehousing: Snowflake, BigQuery, Redshift & DuckDB - Data Engineering Hub

← Back to Hub Modern Data Warehousing: Snowflake, BigQuery, Redshift & DuckDB Architecture patterns for cloud data warehouses. Covers columnar storage, partitioning, clustering, materialized views, zero-copy cloning, and cost optimization across Snowflake, BigQuery, Redshift, and DuckDB. Table of Contents Architecture Comparison Columnar Storage Fundamentals Partitioning & Clustering Materialized Views Zero-Copy Cloning Cost Optimization DuckDB for Local Analytics Migration Patterns Architecture Comparison Feature Snowflake BigQuery Redshift DuckDB Storage/Compute Separated Separated Coupled (RA3 separates) Embedded Scaling Instant (seconds) Instant (slots) Minutes (resize) N/A (local) Concurrency Multi-cluster WH Slot-based WLM queues Single-process Partitioning Micro-partitions (auto) Partitioned tables Dist/Sort keys Hive partitioning Clustering Clustering keys Clustering columns Sort keys N/A Semi-structured VARIANT (native) JSON/RECORD SUPER type JSON/STRUCT ML Integration Snowpark ML BigQuery ML Redshift ML Python UDFs Columnar Storage Fundamentals All modern warehouses use columnar storage with compression: How It Works Column chunks: Each column stored separately, enabling column pruning Encoding: Dictionary, RLE, Delta, Bit-packing per data type Compression: ZSTD, Snappy, LZ4 on encoded data Statistics: Min/max/null count per chunk for predicate pushdown Snowflake Micro-Partitions -- Auto-clustering: Snowflake manages micro-partitions (50-500MB) -- No manual partitioning needed, but clustering keys help large tables CREATE TABLE events ( event_id STRING, user_id STRING, event_time TIMESTAMP_NTZ, payload VARIANT ) CLUSTER BY (user_id, event_time); -- Check clustering depth (lower = better pruning) SELECT SYSTEM$CLUSTERING_DEPTH('EVENTS'); -- Recluster manually if needed ALTER TABLE EVENTS RECLUSTER; BigQuery Partitioned & Clustered Tables -- Partition by date (ingestion or column) CREATE TABLE `project.dataset.events` PARTITION BY DATE(event_time) CLUSTER BY user_id, event_type AS SELECT * FROM `project.dataset.raw_events`; -- Partition pruning happens automatically SELECT * FROM `project.dataset.events` WHERE DATE(event_time) = '2024-01-15' AND user_id = 'user_123'; -- Cluster pruning also applies -- Partition expiration for cost control ALTER TABLE `project.dataset.events` SET OPTIONS (partition_expiration_days = 365); Redshift Distribution & Sort Keys -- Distribution: EVEN (default), KEY (co-locate joins), ALL (small tables) -- Sort: COMPOUND (prefix) or INTERLEAVED (equal weight) CREATE TABLE events ( event_id BIGINT IDENTITY(1,1), user_id BIGINT, event_time TIMESTAMP, payload SUPER ) DISTKEY (user_id) -- Co-locate user events SORTKEY (event_time); -- Time-series queries -- ANALYZE for query optimizer statistics ANALYZE events; Partitioning & Clustering Strategies When to Partition vs Cluster Partition: High-cardinality filter columns (date, tenant_id) — reduces bytes scanned Cluster: Medium-cardinality columns used in joins/filters — co-locates related data Both: Partition by date, cluster by user_id within each partition Snowflake: Clustering Keys -- Add clustering to existing table ALTER TABLE events CLUSTER BY (user_id, event_time); -- Monitor clustering effectiveness SELECT * FROM TABLE(INFORMATION_SCHEMA.CLUSTERING_INFORMATION('EVENTS')); -- clustering_depth < 10 is good; > 100 needs reclustering -- Automatic clustering (Snowflake manages) ALTER TABLE events RESUME RECLUSTER; ALTER TABLE events SUSPEND RECLUSTER; BigQuery: Partition + Cluster -- Ingestion-time partitioning (cheaper, less flexible) CREATE TABLE `dataset.events` PARTITION BY _PARTITIONDATE CLUSTER BY user_id, event_type OPTIONS (partition_expiration_days = 730); -- Column partitioning (flexible, supports partition pruning on any DATE/TIMESTAMP) CREATE TABLE `dataset.events` PARTITION BY DATE(event_time) CLUSTER BY user_id, event_type; -- Require partition filter (prevents full scans) ALTER TABLE `dataset.events` SET OPTIONS (require_partition_filter = TRUE); Redshift: Distribution Styles -- Choose diststyle per table size and join patterns -- EVEN: Default, round-robin (good for uniform access) -- KEY: Hash on distkey (co-locate joined tables) -- ALL: Broadcast to all nodes (dimension tables < 10M rows) -- Star schema: fact table KEY on FK, dimension tables ALL CREATE TABLE fact_sales ( sale_id BIGINT, customer_id BIGINT, product_id BIGINT, amount DECIMAL(10,2) ) DISTKEY (customer_id) SORTKEY (sale_date); CREATE TABLE dim_customer ( customer_id BIGINT, name VARCHAR(100) ) DISTSTYLE ALL; -- Compound vs Interleaved sort keys -- COMPOUND: WHERE sale_date > '2024-01-01' AND customer_id = 123 -- INTERLEAVED: WHERE customer_id = 123 OR product_id = 456 Materialized Views Pre-computed results for expensive aggregations. Auto-refreshed on base table changes. Snowflake Materialized Views CREATE MATERIALIZED VIEW mv_daily_user_stats AS SELECT DATE(event_time) AS event_date, user_id, COUNT(*) AS event_count, COUNT(DISTINCT event_type) AS unique_events FROM events GROUP BY DATE(event_time), user_id; -- Query rewrite is automatic for compatible queries SELECT event_date, user_id, event_count FROM events WHERE event_date >= '2024-01-01' GROUP BY event_date, user_id; -- Rewrites to MV -- Suspend/resume for maintenance ALTER MATERIALIZED VIEW mv_daily_user_stats SUSPEND; ALTER MATERIALIZED VIEW mv_daily_user_stats RESUME; BigQuery Materialized Views CREATE MATERIALIZED VIEW `project.dataset.mv_daily_stats` PARTITION BY event_date CLUSTER BY user_id OPTIONS ( enable_refresh = TRUE, refresh_interval_minutes = 60 ) AS SELECT DATE(event_time) AS event_date, user_id, COUNT(*) AS event_count, COUNT(DISTINCT event_type) AS unique_events FROM `project.dataset.events` GROUP BY event_date, user_id; -- Manual refresh if needed CALL BQ.REFRESH_MATERIALIZED_VIEW('project.dataset.mv_daily_stats'); Redshift Materialized Views CREATE MATERIALIZED VIEW mv_daily_user_stats DISTKEY (user_id) SORTKEY (event_date) AS SELECT DATE(event_time) AS event_date, user_id, COUNT(*) AS event_count, COUNT(DISTINCT event_type) AS unique_events FROM events GROUP BY DATE(event_time), user_id; -- Auto-refresh on commit (default) or manual REFRESH MATERIALIZED VIEW mv_daily_user_stats; Zero-Copy Cloning Instant, metadata-only copies for dev/test/backup without storage duplication. Snowflake: CLONE -- Clone entire database (instant, zero storage) CREATE DATABASE dev_db CLONE prod_db; CREATE SCHEMA dev_schema CLONE prod_schema; CREATE TABLE dev_table CLONE prod_table; -- Time Travel: Clone at specific point CREATE TABLE events_yesterday CLONE events AT(TIMESTAMP => DATEADD(day, -1, CURRENT_TIMESTAMP())); -- Clone with data filtering (CREATE TABLE AS SELECT from clone) CREATE TABLE dev_sample AS SELECT * FROM prod_table CLONE TABLE prod_table TABLESAMPLE BERNOULLI (10); BigQuery: Table Clones (Preview) -- Create clone (metadata only) CREATE TABLE `project.dev.events` CLONE `project.prod.events`; -- Clone with time travel CREATE TABLE `project.dev.events_20240101` CLONE `project.prod.events` FOR SYSTEM_TIME AS OF '2024-01-01 00:00:00'; Redshift: Not Native (Use CTAS) -- No zero-copy clone. Use CTAS (copies data) or UNLOAD/LOAD CREATE TABLE dev_events AS SELECT * FROM prod_events WHERE 1=0; -- Schema only INSERT INTO dev_events SELECT * FROM prod_events TABLESAMPLE BERNOULLI (10); Cost Optimization Strategies Storage Costs Compression: All warehouses auto-compress; verify with SYSTEM$ESTIMATE_SEARCH_OPTIMIZATION_COSTS (Snowflake) Data types: Use smallest type (INT vs BIGINT, DATE vs TIMESTAMP) VARIANT/SUPER/JSON: Expensive to query; extract frequent fields to columns Retention: Set TTL on temp tables; use partitioning for time-based expiry Compute Costs Warehouse Lever Action Snowflake Warehouse size XS for dev, L/XL for prod; auto-suspend 60s Snowflake Multi-cluster Max clusters = peak concurrency / 8 BigQuery Slot reservations Commit 1yr/3yr for 40-60% discount BigQuery On-demand Partition + cluster to reduce bytes billed Redshift RA3 + Redshift Serverless Serverless for variable workloads Redshift Concurrency scaling Auto-add clusters for queue spikes Query Optimization for Cost -- Snowflake: Search optimization service (for point lookups on non-clustered cols) ALTER TABLE events ADD SEARCH OPTIMIZATION ON EQUALITY(user_id); -- BigQuery: Partition pruning verification EXPLAIN SELECT * FROM `project.dataset.events` WHERE DATE(event_time) = '2024-01-15'; -- Check "partitionId" in explain output -- Redshift: WLM queue configuration -- Short queries (dashboard): dedicated queue, high priority -- ETL: separate queue, lower priority -- Ad-hoc: default queue, timeout 30min DuckDB for Local Analytics Embedded analytical database for local development, testing, and edge analytics. Key Features Zero-dep: Single binary, no server, runs in-process SQL compatibility: PostgreSQL dialect, window functions, CTEs Columnar: Vectorized execution, parallel query processing Extensions: HTTPFS (S3), Parquet, Iceberg, Delta, Postgres scanner Local Development Workflow import duckdb # Connect to local file or in-memory con = duckdb.connect("local_warehouse.duckdb") # Read Parquet directly from S3 (httpfs extension) con.install_extension("httpfs") con.load_extension("httpfs") con.execute(""" CREATE VIEW events AS SELECT * FROM read_parquet('s3://bucket/events/year=2024/month=*/*.parquet') """) # Query with full SQL result = con.execute(""" SELECT DATE(event_time) as day, user_id, COUNT(*) FROM events WHERE event_time >= '2024-01-01' GROUP BY day, user_id ORDER BY day DESC """).fetchdf() # Export to Parquet for warehouse loading con.execute("COPY (SELECT * FROM events) TO 'local_events.parquet' (FORMAT PARQUET)") DuckDB + dbt for Local Modeling # profiles.yml duckdb: target: dev outputs: dev: type: duckdb path: local_warehouse.duckdb threads: 4 # dbt models run locally against DuckDB # dbt run --target dev # Then deploy same models to Snowflake/BigQuery by changing target Migration Patterns Snowflake → BigQuery -- 1. Extract DDL (Snowflake) SELECT GET_DDL('TABLE', 'EVENTS'); -- 2. Convert data types -- VARIANT → JSON/RECORD -- TIMESTAMP_NTZ → TIMESTAMP -- NUMBER(p,s) → NUMERIC(p,s) -- 3. Load via GCS (recommended for > TB) -- Snowflake: COPY INTO @stage FROM events; -- gsutil cp @stage/* gs://bucket/migration/ -- BigQuery: bq load --source_format=PARQUET dataset.events gs://bucket/migration/*.parquet -- 4. Validate row counts & checksums SELECT COUNT(*), SUM(HASH(*)), ... FROM both; Redshift → Snowflake -- Use AWS DMS or Snowflake's Redshift migration tool -- Key conversions: -- DISTKEY/SORTKEY → CLUSTER BY -- SUPER → VARIANT -- IDENTITY → AUTOINCREMENT -- INTERLEAVED SORTKEY → not directly supported (use clustering) -- Redshift UNLOAD to S3 → Snowflake COPY FROM S3 UNLOAD ('SELECT * FROM events') TO 's3://bucket/migration/events_' IAM_ROLE 'arn:aws:iam::...' PARQUET PARTITION BY (year, month); -- Snowflake COPY INTO events FROM @s3_stage/migration/ FILE_FORMAT = (TYPE = PARQUET); Related Guides Batch Processing at Scale Real-Time Streaming Data Quality Contracts Data Observability

Comments

Popular posts from this blog

Firmware Testing Strategies: Hardware-in-the-Loop, Simulation, and CI/CD Pipelines | Embedded Systems Hub

IoT Protocols Compared: MQTT, CoAP, HTTP/2, WebSocket - Performance, Overhead, and Use Cases | Embedded Systems Hub

Embedded Security: Secure Boot, ARM TrustZone, and OTA Update Integrity | Embedded Systems Hub