Engine-Specific SQL Guide
Key behavioural differences across the four most common data warehouse engines. Check this when a query will be run on a specific engine, or when porting a query between engines.
Snowflake
Date/Time
CURRENT_TIMESTAMPreturns aTIMESTAMP_LTZby default — useCONVERT_TIMEZONE('UTC', CURRENT_TIMESTAMP)for consistency.DATE_TRUNC('month', col)returns a DATE; truncation granularity strings must be lowercase or uppercase (not mixed).DATEADD(day, -7, CURRENT_DATE)— Snowflake supports DATEADD natively.- String-to-date: prefer explicit
TO_DATE(col, 'YYYY-MM-DD')over implicit casting.
Performance
- Micro-partition pruning: filter on columns that are naturally ordered (e.g.
created_at) to leverage micro-partition metadata. - Clustering keys: for very large tables (> 500M rows), check if a clustering key exists on the filter column — use
SYSTEM$CLUSTERING_INFORMATION('<table>'). - RESULT_CACHE: Snowflake caches identical query results for 24 hours. Use
ALTER SESSION SET USE_CACHED_RESULT = FALSEfor benchmarking. - VARIANT / SEMI-STRUCTURED: accessing nested JSON with
col:field::stringis efficient; avoid parsing entire VARIANT columns unnecessarily.
Gotchas
ILIKEis case-insensitive LIKE — useful for string matching on user input.QUALIFYcan replace outer subqueries for window function filtering.- CTEs are materialised by default when referenced more than once in some query plans — no need to force materialisation with a temp table.
BigQuery
Date/Time
CURRENT_DATE()/CURRENT_TIMESTAMP()— note the parentheses (function call syntax).DATE_TRUNC(date_col, MONTH)— second argument is a keyword, not a string.TIMESTAMP_DIFF(ts1, ts2, DAY)— argument order matters.- BigQuery stores timestamps in UTC; use
DATETIMEtype for local-timezone-aware data.
Performance
- Partitioning: always filter on the partition column (
_PARTITIONTIMEor a declared partition field). Unfiltered partition scans process the full table. - Clustering: cluster on columns used in GROUP BY and WHERE after the partition column. Use
INFORMATION_SCHEMA.TABLE_STORAGEto verify clustering is in effect. - Wildcard tables:
FROM project.dataset.events_*withWHERE _TABLE_SUFFIX BETWEEN '20240101' AND '20240131'— always restrict with_TABLE_SUFFIX. - Flattening arrays:
UNNEST()is needed to expand ARRAY columns — each unnested element creates a new row.
Gotchas
ARRAY_AGGwithoutIGNORE NULLSincludes NULLs in the array.COUNTIF(condition)is BigQuery-specific shorthand forSUM(CASE WHEN condition THEN 1 ELSE 0 END).- Standard SQL dialect vs. legacy SQL: always use Standard SQL (
#standardSQLor project-default setting).
PostgreSQL
Date/Time
NOW()returnsTIMESTAMP WITH TIME ZONEin the server timezone.DATE_TRUNC('month', col)returnsTIMESTAMP, notDATE— cast with::DATEif needed.INTERVAL: usecol + INTERVAL '7 days'(quoted, with unit).EXTRACT(epoch FROM ts)for Unix timestamp conversion.
Performance
- Index use:
WHERE LOWER(email) = 'x@y.com'prevents index use — create a functional indexON users(LOWER(email))or store email pre-lowercased. - EXPLAIN ANALYZE: always use both keywords —
EXPLAINalone shows estimated plan;ANALYZEexecutes and shows actual times. - Vacuum/Analyze: stale statistics cause bad query plans. Run
ANALYZE <table>after large data loads. - CTE fence: in Postgres < 12, CTEs are optimisation fences (always materialised). In Postgres ≥ 12, the planner may inline them. Use
WITH MATERIALIZED/WITH NOT MATERIALIZEDto be explicit.
Gotchas
SERIAL/BIGSERIALare not true types — they're shorthand for sequences. UseGENERATED ALWAYS AS IDENTITYfor new tables.||for string concatenation (not+).ILIKEfor case-insensitive pattern matching.
Redshift
Date/Time
GETDATE()returns current time in UTC (not server TZ).DATEADD(day, -7, GETDATE())— similar to Snowflake; supported natively.DATEDIFF(day, start, end)— argument order is unit, start, end (not start, end, unit like some other engines).
Performance
- Distribution key (DISTKEY): used for join colocation. JOIN on a non-DISTKEY causes data redistribution. Use
SVV_TABLE_INFOto check. - Sort key (SORTKEY): filter on the leading sort key column to enable zone map pruning. Check with
SVL_QUERY_SUMMARY. - VACUUM / ANALYZE: Redshift needs periodic VACUUM to reclaim space from deletes and re-sort data. Monitor with
SVV_TABLE_INFO.unsorted_pct. - Result caching: enabled by default; disable with
SET enable_result_cache_for_session = offfor benchmarking.
Gotchas
- Maximum VARCHAR is 65535 bytes — large JSON stored as text hits this.
LISTAGGis Redshift's equivalent ofSTRING_AGG/GROUP_CONCAT.- Redshift does not enforce primary key / unique constraints — duplicates can exist silently.
- Window functions cannot be nested; use a subquery to apply a window function to another window function's output.