Snowflake Query Analyzer
Expert skill for Snowflake query performance analysis and optimization.
When to Use This Skill
Activate when the user mentions:
- "Optimize this Snowflake query"
- "Why is this query slow"
- "Reduce Snowflake costs"
- "Analyze query performance"
- "Clustering keys"
- "Query profile"
- "Warehouse sizing"
- "Partition pruning"
Deterministic Analysis Tool
The plugin includes a Python script for direct Snowflake query analysis:
# Analyze specific query
python ${CLAUDE_PLUGIN_ROOT}/scripts/analyze_snowflake.py --query-id <query_id>
# Find queries for a model
python ${CLAUDE_PLUGIN_ROOT}/scripts/analyze_snowflake.py --model fct_orders
# Find slow queries
python ${CLAUDE_PLUGIN_ROOT}/scripts/analyze_snowflake.py --slow --threshold 60 --limit 20
# Find expensive queries
python ${CLAUDE_PLUGIN_ROOT}/scripts/analyze_snowflake.py --expensive --limit 20
# Get table clustering info
python ${CLAUDE_PLUGIN_ROOT}/scripts/analyze_snowflake.py --table my_table
This script:
- Connects directly to Snowflake INFORMATION_SCHEMA
- Extracts query metadata deterministically
- Calculates partition pruning, spilling, and cost metrics
- Provides structured JSON output (reduces token consumption)
- Identifies performance issues automatically
Requirements:
snowflake-connector-pythoninstalled- Snowflake credentials in environment or
~/.dbt/profiles.yml
Core Capabilities
1. Query Profile Analysis
Key metrics to analyze from Snowflake query profiles:
Execution Time Breakdown
- Compilation time
- Queuing time
- Execution time per operator
- Network communication time
Data Processing
- Bytes scanned
- Bytes written
- Partition pruning percentage
- Micro-partition overlap
Resource Usage
- Warehouse size used
- Credits consumed
- Spilling to local disk
- Spilling to remote storage
Parallelism
- Number of nodes/threads
- Data distribution skew
- Operator parallelization
2. Common Performance Issues
Issue 1: Poor Partition Pruning
Symptom: Pruning percentage < 50%
Cause: Filters not aligned with clustering keys
Impact: Scanning unnecessary data, slow queries
Example:
Table clustered by (date_column)
Query filters on customer_id
Result: Scans entire table
Fix: Add clustering key for customer_id OR add date filter
Issue 2: Spilling to Disk
Symptom: "Bytes spilled to local/remote storage" > 0
Cause: Insufficient warehouse memory
Impact: 10-100x slower performance
Fix Options:
1. Increase warehouse size (S → M → L)
2. Optimize query to reduce memory (remove unnecessary columns)
3. Break into smaller queries
4. Add filters earlier in query
Issue 3: Exploding Joins
Symptom: Rows output >> rows input
Cause: Cartesian product or many-to-many joins
Impact: Memory issues, timeouts
Detection:
- Look for joins without proper keys
- Check for duplicate keys
- Verify join conditions
Fix:
- Add deduplication before join
- Use window functions instead
- Ensure proper join keys
Issue 4: No Clustering
Symptom: Average clustering depth > 50
Cause: Table not clustered or poorly maintained
Impact: Full table scans
Fix:
ALTER TABLE table_name CLUSTER BY (col1, col2);
Issue 5: Inefficient Aggregations
Symptom: Long execution time on GROUP BY
Cause: High cardinality group by, late aggregation
Impact: Excessive memory and compute
Fix:
- Aggregate earlier in query
- Consider materialized aggregates
- Use approximate aggregations (HLL, APPROX_COUNT_DISTINCT)
3. Optimization Strategies
Strategy 1: Clustering Key Design
Best practices:
-- Good: Frequently filtered columns, time-based
CLUSTER BY (date_column, category_id)
-- Consider:
- Columns in WHERE clauses
- Columns in JOIN conditions
- Cardinality: high-to-low (date before ID)
- Limit to 3-4 columns
- Order matters: most selective first
-- Don't cluster on:
- Very high cardinality (IDs with no time component)
- Columns never in WHERE/JOIN
- Frequently updated columns
Strategy 2: Incremental Processing
-- Instead of full table scan:
SELECT * FROM large_table
WHERE process_date >= CURRENT_DATE - 7
-- Use incremental logic:
{{ config(
materialized='incremental',
unique_key='id',
incremental_strategy='delete+insert'
) }}
SELECT *
FROM {{ source('raw', 'large_table') }}
{% if is_incremental() %}
WHERE updated_at > (SELECT MAX(updated_at) FROM {{ this }})
{% endif %}
Strategy 3: Result Caching
-- Enable result cache (24 hour default)
ALTER SESSION SET USE_CACHED_RESULT = TRUE;
-- Same query returns instantly if:
- Query text identical
- Tables unchanged
- Within cache TTL (24 hours)
Strategy 4: Warehouse Sizing
Decision matrix:
Workload Type → Recommended Size
Single large query, lots of data → L or XL
Many concurrent small queries → Multi-cluster S or M
Mixed workload → Separate warehouses
Development/testing → XS or S
ETL/batch processing → M or L
BI dashboards → M with auto-suspend=60s
Auto-scaling configuration:
CREATE WAREHOUSE analytics_wh
WAREHOUSE_SIZE = 'MEDIUM'
AUTO_SUSPEND = 60
AUTO_RESUME = TRUE
MIN_CLUSTER_COUNT = 1
MAX_CLUSTER_COUNT = 3
SCALING_POLICY = 'STANDARD';
4. Cost Analysis
Calculate Query Cost
-- Get query execution details
SELECT
query_id,
query_text,
warehouse_size,
execution_time / 1000 as execution_seconds,
bytes_scanned,
-- Estimate cost (approximate)
(execution_time / 1000.0 / 3600) *
CASE warehouse_size
WHEN 'X-Small' THEN 1
WHEN 'Small' THEN 2
WHEN 'Medium' THEN 4
WHEN 'Large' THEN 8
WHEN 'X-Large' THEN 16
END as estimated_credits
FROM table(information_schema.query_history())
WHERE query_id = '<QUERY_ID>'
Cost Optimization Checklist
- [ ] Appropriate warehouse size (not oversized)
- [ ] Auto-suspend enabled (60-300 seconds)
- [ ] Clustering maintained on large tables
- [ ] Incremental processing where possible
- [ ] No full table scans on large tables (>1M rows)
- [ ] Partition pruning > 80%
- [ ] Result cache utilized
- [ ] No unnecessary column selection (SELECT *)
- [ ] Separate warehouses for different workloads
5. Query Rewriting Patterns
Pattern 1: Push Down Filters
-- ❌ Bad: Filter after expensive operations
SELECT customer_id, total
FROM (
SELECT
customer_id,
SUM(amount) as total
FROM large_table
GROUP BY customer_id
)
WHERE customer_id IN (1, 2, 3)
-- ✅ Good: Filter early
SELECT
customer_id,
SUM(amount) as total
FROM large_table
WHERE customer_id IN (1, 2, 3)
GROUP BY customer_id
Pattern 2: Use CTEs for Clarity and Optimization
-- ❌ Bad: Repeated subqueries
SELECT a.*,
(SELECT COUNT(*) FROM orders WHERE customer_id = a.id) as order_count,
(SELECT SUM(total) FROM orders WHERE customer_id = a.id) as total_spent
FROM customers a
-- ✅ Good: Single scan with CTE
WITH order_stats AS (
SELECT
customer_id,
COUNT(*) as order_count,
SUM(total) as total_spent
FROM orders
GROUP BY customer_id
)
SELECT
a.*,
COALESCE(b.order_count, 0) as order_count,
COALESCE(b.total_spent, 0) as total_spent
FROM customers a
LEFT JOIN order_stats b ON a.id = b.customer_id
**Pattern 3: Avoid SELECT ***
-- ❌ Bad: Unnecessary columns increase data transfer
SELECT * FROM large_table WHERE id = 123
-- ✅ Good: Only needed columns
SELECT id, name, amount, date
FROM large_table
WHERE id = 123
Pattern 4: Use QUALIFY for Window Functions
-- ❌ Bad: Subquery for window function filter
SELECT * FROM (
SELECT
*,
ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY date DESC) as rn
FROM orders
)
WHERE rn = 1
-- ✅ Good: QUALIFY clause (Snowflake-specific)
SELECT *
FROM orders
QUALIFY ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY date DESC) = 1
6. Monitoring Queries
Query for Long-Running Queries
SELECT
query_id,
user_name,
warehouse_name,
execution_status,
total_elapsed_time / 1000 as seconds,
bytes_scanned / POWER(1024, 3) as gb_scanned,
query_text
FROM table(information_schema.query_history(
dateadd('hours', -24, current_timestamp()),
current_timestamp()
))
WHERE execution_status = 'SUCCESS'
AND total_elapsed_time > 60000 -- > 1 minute
ORDER BY total_elapsed_time DESC
LIMIT 20;
Query for Expensive Queries
SELECT
query_id,
start_time,
end_time,
warehouse_size,
(execution_time / 1000.0 / 3600) *
CASE warehouse_size
WHEN 'MEDIUM' THEN 4
WHEN 'LARGE' THEN 8
END as estimated_credits,
query_text
FROM table(information_schema.query_history())
WHERE start_time > dateadd('day', -7, current_timestamp())
ORDER BY estimated_credits DESC
LIMIT 20;
7. Diagnosis Workflow
Step 1: Identify the Problem
Questions to ask:
- Is it slow (execution time)?
- Is it expensive (credits consumed)?
- Does it timeout?
- Does it produce wrong results?
Step 2: Get Query Profile
-- In Snowflake UI: History → Click Query → Query Profile
-- Or use SQL:
SELECT *
FROM table(information_schema.query_history())
WHERE query_id = '<QUERY_ID>';
Step 3: Analyze Key Metrics
Check:
✓ Partition pruning % (want > 80%)
✓ Bytes spilled (want = 0)
✓ Parallelism (should utilize all nodes)
✓ Operator times (find slowest)
✓ Rows at each stage (detect explosions)
Step 4: Identify Root Cause
Common causes:
- No partition pruning → Add clustering or filters
- Spilling → Increase warehouse size
- Cartesian join → Fix join conditions
- Full table scan → Add indexes/clustering
- High cardinality GROUP BY → Pre-aggregate or sample
Step 5: Implement Fix
Apply optimization:
- Rewrite query
- Add clustering keys
- Change warehouse size
- Use incremental strategy
- Add filters
Step 6: Measure Improvement
Compare before/after:
- Execution time
- Credits consumed
- Bytes scanned
- Partition pruning %
Practical Examples
Example 1: Optimize Slow Aggregation
-- Problem: 5 minutes, 20 credits
-- ❌ Original query
SELECT
customer_id,
DATE_TRUNC('month', order_date) as month,
COUNT(*) as order_count,
SUM(total_amount) as revenue
FROM orders
GROUP BY customer_id, month
-- Analysis:
-- - Full table scan (1B rows)
-- - No partition pruning
-- - No clustering
-- ✅ Solution 1: Add date filter
SELECT
customer_id,
DATE_TRUNC('month', order_date) as month,
COUNT(*) as order_count,
SUM(total_amount) as revenue
FROM orders
WHERE order_date >= '2023-01-01' -- Last 2 years
GROUP BY customer_id, month
-- ✅ Solution 2: Create incremental mart
{{ config(materialized='incremental') }}
-- Build incrementally, 10x faster
Example 2: Fix Spilling Issue
Problem: Query spilling 50GB to remote storage
Analysis from query profile:
- Using SMALL warehouse
- Complex joins with large tables
- Memory exceeded
Fix:
1. Increase warehouse: SMALL → MEDIUM
2. Result: No more spilling, 5x faster
3. Cost: 2x credits but completes vs timing out
Example 3: Optimize with Clustering
-- Problem: 2-minute query on 100M row table
-- Check current clustering
SELECT SYSTEM$CLUSTERING_INFORMATION(
'my_table',
'(order_date, customer_id)'
);
-- Result: average_depth = 180 (bad)
-- Add clustering
ALTER TABLE my_table
CLUSTER BY (order_date, customer_id);
-- After clustering:
-- - Query time: 2min → 8sec (15x faster)
-- - Partition pruning: 5% → 95%
-- - Bytes scanned: 50GB → 2.5GB
Output Format
When analyzing a query, provide:
# Query Analysis Report
## Query Overview
- Query ID: xxx
- Execution Time: X seconds
- Warehouse: X-SMALL
- Credits Consumed: ~X.XX
## Performance Metrics
- Bytes Scanned: XX GB
- Partition Pruning: XX%
- Bytes Spilled: XX GB
- Parallelism: XX nodes
## Issues Identified
🔴 Critical:
- [Issue with high impact]
🟡 Optimization Opportunities:
- [Improvements available]
## Recommendations
### Immediate Actions
1. [Quick win optimization]
2. [Another easy fix]
### Long-term Improvements
1. [Structural change]
2. [Architecture improvement]
## Estimated Impact
- Time Reduction: XX%
- Cost Reduction: XX%
## Implementation Guide
[Step-by-step fix instructions]
Quality Checklist
- [ ] All key metrics analyzed
- [ ] Root cause identified
- [ ] Specific recommendations provided
- [ ] Before/after comparison included
- [ ] Cost impact estimated
- [ ] Implementation steps clear
- [ ] Prevention strategies mentioned
Scan to join WeChat group