| name | data-reference |
| description | Complete data engineering best practices reference covering dbt, SQL transformations, StarRocks/Snowflake/BigQuery patterns, JSON handling, window functions, incremental strategies, and data quality. Load this when you need to verify a data pattern during code review.
|
Data Engineering Reference — dbt, SQL & Warehouse Best Practices
dbt Patterns
Materialization Selection
{{ config(materialized='ephemeral') }}
{{ config(materialized='view') }}
{{ config(materialized='table') }}
{{ config(materialized='incremental', unique_key='id') }}
{{ config(
materialized='materialized_view',
refresh_method='ASYNC EVERY (INTERVAL 1 HOUR)'
) }}
Incremental Model Pattern
{{ config(materialized='incremental', unique_key='id') }}
SELECT ...
FROM {{ ref('source_model') }}
{% if is_incremental() %}
WHERE updated_at >= (
SELECT DATE_ADD(MAX(updated_at), INTERVAL -30 MINUTE)
FROM {{ this }}
)
{% endif %}
{% if is_incremental() %}
WHERE updated_at > (SELECT MAX(updated_at) FROM {{ this }})
{% endif %}
Source vs Ref
FROM {{ source('payment_ms', 'transactions') }}
FROM {{ ref('stg_transactions') }}
FROM payment_ms_prod.transactions
CDC Deduplication
WITH ranked AS (
SELECT *,
ROW_NUMBER() OVER (
PARTITION BY primary_key
ORDER BY updated_at DESC
) AS rn
FROM {{ ref('incoming_model') }}
)
SELECT * FROM ranked WHERE rn = 1
SELECT * FROM {{ ref('incoming_model') }}
SQL Correctness Patterns
Window Functions After GROUP BY
SELECT
channel_id,
category,
COUNT(*) AS count_per_category,
SUM(COUNT(*)) OVER (PARTITION BY channel_id) AS total_for_channel
FROM messages
GROUP BY channel_id, category;
SELECT
channel_id,
category,
COUNT(*) AS count_per_category,
COUNT(*) OVER (PARTITION BY channel_id) AS total_for_channel
FROM messages
GROUP BY channel_id, category;
MIN(MIN(timestamp)) OVER (PARTITION BY channel_id) AS earliest_across_all_types
MAX(MAX(timestamp)) OVER (PARTITION BY channel_id) AS latest_across_all_types
MIN(timestamp)
MAX(timestamp)
NULL Pitfalls
WHERE status = NULL
WHERE status IS NULL
WHERE id NOT IN (SELECT id FROM other WHERE id IS NULL)
WHERE id NOT IN (SELECT id FROM other WHERE id IS NOT NULL)
COALESCE(SUM(amount), 0)
SUM(COALESCE(amount, 0))
JOIN Pitfalls
SELECT a.*, b.name
FROM orders a
LEFT JOIN customers b ON a.customer_id = b.id
WHERE b.status = 'active'
LEFT JOIN customers b ON a.customer_id = b.id AND b.status = 'active'
SELECT o.*, p.name
FROM orders o
JOIN order_items p ON o.id = p.order_id
JSON Handling
StarRocks JSON Functions
get_json_string(column, '$.field')
get_json_string(column, '$.nested.field')
get_json_string(column, '$[0].field')
get_json_int(column, '$.count')
get_json_double(column, '$.score')
parse_json(varchar_column)
SELECT t.value
FROM table_name,
json_each(parse_json(array_column)) t
json_length(parse_json(array_column))
LENGTH(x) - LENGTH(REPLACE(x, ',', '')) + 1
Snowflake JSON Functions
column:field::VARCHAR
column:nested.field::INT
column[0]:field::VARCHAR
SELECT f.value
FROM table_name,
LATERAL FLATTEN(input => array_column) f
ARRAY_SIZE(array_column)
BigQuery JSON Functions
JSON_EXTRACT_SCALAR(column, '$.field')
SELECT element
FROM table_name,
UNNEST(JSON_EXTRACT_ARRAY(column, '$.array')) AS element
ARRAY_LENGTH(JSON_EXTRACT_ARRAY(column, '$.array'))
JSON Array to CSV Conversion
REPLACE(REPLACE(REPLACE(REPLACE(
'["tool_a","tool_b"]',
'["', ''), '"]', ''), '","', ','), '"', '')
SELECT get_json_string(CAST(t.value AS VARCHAR), '$') AS tool_name
FROM json_each(parse_json(tools_array)) t
SELECT GROUP_CONCAT(
get_json_string(CAST(t.value AS VARCHAR), '$')
) AS tools_csv
FROM json_each(parse_json(tools_array)) t
Data Quality Patterns
Array Size Counting
CASE
WHEN x IS NULL OR x = '[]' THEN 0
ELSE (LENGTH(x) - LENGTH(REPLACE(x, ',', ''))) + 1
END
CASE
WHEN x IS NULL OR x IN ('[]', '') THEN 0
ELSE json_length(parse_json(x))
END
COALESCE(ARRAY_SIZE(x), 0)
COALESCE(ARRAY_LENGTH(JSON_EXTRACT_ARRAY(x, '$')), 0)
Phantom Values from Empty Arrays
CASE
WHEN sources_used IS NULL OR sources_used IN ('[]', '[""]', '')
THEN NULL
ELSE <extraction logic>
END AS tools_used
WHERE tools_used IS NOT NULL
AND tools_used != ''
AND tools_used != '[]'
VARCHAR Truncation
CAST(large_json_column AS VARCHAR(65000))
SELECT COUNT(*) FROM source_table
WHERE LENGTH(json_column) > 64000
StarRocks-Specific Patterns
Table Configuration
{{ config(
table_type='PRIMARY',
keys=['id'],
distributed_by=['id'],
buckets=8
) }}
{{ config(
table_type='DUPLICATE',
distributed_by=['event_date'],
buckets=16,
partition_by=["date_trunc('month', event_date)"]
) }}
Distribution Strategy
distributed_by=['user_id']
distributed_by=['status']
buckets=4
buckets=8
buckets=16
buckets=32
Materialized View Refresh
refresh_method='ASYNC EVERY (INTERVAL 1 MINUTE)'
refresh_method='ASYNC EVERY (INTERVAL 1 HOUR)'
refresh_method="ASYNC START('2025-01-01 00:30:00') EVERY (INTERVAL 1 DAY)"
refresh_method='MANUAL'
Medallion Architecture
Layer Responsibilities
Bronze (incoming/staging):
- Raw data from sources
- Type casting only (CAST to proper types)
- No business logic
- Ephemeral in dbt (no physical table)
- 1:1 mapping with source tables
Silver (intermediate/cleaned):
- Flatten nested structures (JSON → columns)
- Deduplicate (ROW_NUMBER for CDC)
- Resolve business keys
- Drop heavy/unnecessary fields
- Apply data quality rules
- This is the "single source of truth"
Gold (marts/aggregated):
- Pre-aggregated for specific use cases
- Optimized for dashboard queries
- May denormalize for read performance
- Simple aggregations over Silver
- Should be fast to refresh
Anti-Patterns
BAD: Bronze → Gold (skip Silver)
- No deduplication
- No data quality layer
- No audit trail
- Gold becomes complex and slow
BAD: Business logic in Bronze
- Bronze should be a pure mirror of the source
- Transformations belong in Silver
BAD: Complex JOINs in Gold
- Gold should aggregate Silver, not join raw sources
- Heavy JOINs belong in Silver
BAD: Silver depends on Gold
- Creates circular dependency
- Gold is derived from Silver, never the reverse
Common Anti-Patterns
| Anti-Pattern | Why It's Bad | Fix |
|---|
| Comma counting for JSON array size | Breaks on nested objects, strings with commas | json_length(parse_json(x)) |
COUNT(*) OVER after GROUP BY | Counts groups, not rows | SUM(COUNT(*)) OVER |
REPLACE chain for JSON→CSV | Order-dependent, breaks on edge cases | Keep JSON, use json_each() at point of use |
| No lookback window in incremental | Misses late-arriving data | MAX(updated_at) - INTERVAL 30 MINUTE |
VARCHAR(N) for large JSON | Silent truncation | Use STRING/TEXT or add length checks |
| Hardcoded schema names in dbt | Breaks environment portability | Use {{ var(target.name)['environment'] }} |
No unique test on PKs | Duplicates go undetected | Add dbt_expectations tests |
| MV refresh < 1 minute | Wastes compute, may not finish before next refresh | Match refresh to actual data freshness needs |
LEFT JOIN + WHERE on right table | Silently becomes INNER JOIN | Move filter to ON clause |
SELECT * in incremental models | Schema changes break the model | Explicit column list |