Skip to content

Module 1: Quick Syntax & Foundation Cheat Sheet ​

A Data Engineering focused reference covering core SQL syntax, joins, set operations, NULL handling, and type casting using dialect-agnostic ANSI SQL.


Table of Contents ​

  1. Core Operations
  2. Joins & Join Traps
  3. Set Operations
  4. NULL Semantics
  5. Type Casting & Coercion

1. Core Operations ​

SELECT ​

The SELECT statement retrieves columns from one or more tables. It is the foundation of every SQL query and supports expressions, aliases, and the DISTINCT keyword to eliminate duplicate rows.

sql
SELECT [DISTINCT] column1 [AS alias1], column2, expression
FROM table_name;

Data Engineering Example

Source table: raw_events

event_iduser_idevent_typecreated_at
1101page_view2024-01-15 08:00:00
2102click2024-01-15 08:05:00
3101page_view2024-01-15 08:10:00
4103signup2024-01-15 09:00:00
sql
SELECT DISTINCT event_type AS activity
FROM raw_events;

Expected output:

activity
page_view
click
signup

WARNING

Gotcha: SELECT DISTINCT applies to the entire row, not just the first column. SELECT DISTINCT a, b deduplicates on the combination of (a, b), not a alone. In data pipelines this can silently produce more rows than expected if you add a column without realizing it breaks your dedup logic.


WHERE ​

The WHERE clause filters rows before any aggregation. It supports comparison operators (=, <>, <, >, <=, >=), logical operators (AND, OR, NOT), pattern matching (LIKE, ILIKE), range checks (BETWEEN), and membership tests (IN).

sql
SELECT columns
FROM table_name
WHERE condition1 AND (condition2 OR condition3);

Data Engineering Example

Source table: pipeline_runs

run_idpipeline_namestatusduration_secrun_date
1etl_orderssuccess1202024-01-10
2etl_ordersfailed452024-01-11
3etl_userssuccess3002024-01-11
4etl_orderssuccess6002024-01-12
sql
SELECT run_id, pipeline_name, duration_sec
FROM pipeline_runs
WHERE status = 'failed'
   OR (status = 'success' AND duration_sec > 200);

Expected output:

run_idpipeline_nameduration_sec
2etl_orders45
3etl_users300
4etl_orders600

WARNING

Gotcha: Operator precedence — AND binds tighter than OR. The expression A OR B AND C evaluates as A OR (B AND C), not (A OR B) AND C. Always use explicit parentheses to avoid subtle filter bugs in complex pipeline logic.


GROUP BY ​

GROUP BY collapses rows that share the same values in the specified columns into summary rows. Every column in the SELECT list must either appear in the GROUP BY clause or be wrapped in an aggregate function (COUNT, SUM, AVG, MIN, MAX).

sql
SELECT group_col, AGG_FUNC(measure_col) AS alias
FROM table_name
WHERE filter_condition
GROUP BY group_col;

Data Engineering Example

Source table: raw_events (same as above)

sql
SELECT
    user_id,
    COUNT(*)           AS total_events,
    MIN(created_at)    AS first_seen,
    MAX(created_at)    AS last_seen
FROM raw_events
GROUP BY user_id;

Expected output:

user_idtotal_eventsfirst_seenlast_seen
10122024-01-15 08:00:002024-01-15 08:10:00
10212024-01-15 08:05:002024-01-15 08:05:00
10312024-01-15 09:00:002024-01-15 09:00:00

WARNING

Gotcha: In MySQL, non-aggregated columns not in GROUP BY silently return arbitrary values (unless ONLY_FULL_GROUP_BY mode is on). ANSI SQL and most modern engines reject such queries. Always explicitly list every non-aggregated column in GROUP BY.


HAVING ​

HAVING filters after aggregation, whereas WHERE filters before. It is the only way to impose conditions on aggregate values.

sql
SELECT group_col, AGG_FUNC(measure_col) AS alias
FROM table_name
GROUP BY group_col
HAVING AGG_FUNC(measure_col) > threshold;

Data Engineering Example

Source table: daily_loads

load_datesource_systemrow_count
2024-01-10crm15000
2024-01-10erp8000
2024-01-11crm16000
2024-01-11erp200
2024-01-12crm14500
2024-01-12erp9800
sql
SELECT
    source_system,
    AVG(row_count) AS avg_rows
FROM daily_loads
GROUP BY source_system
HAVING AVG(row_count) < 10000;

Expected output:

source_systemavg_rows
erp6000

WARNING

Gotcha: You cannot reference a column alias defined in SELECT inside a HAVING clause in ANSI SQL. You must repeat the aggregate expression: HAVING AVG(row_count) < 10000, not HAVING avg_rows < 10000. Some engines (MySQL, BigQuery) allow it, but it is non-portable.

Active Recall · Cloze Drill

Recall how to filter aggregated groups in ANSI SQL without relying on non-portable SELECT aliases:

New
SELECT source_system, AVG(row_count) AS avg_rows
FROM daily_loads
GROUP BY source_system
;

ORDER BY ​

ORDER BY sorts the final result set. It is the only clause that guarantees row order — without it, the database may return rows in any order across executions.

sql
SELECT columns
FROM table_name
ORDER BY column1 [ASC | DESC] [NULLS FIRST | NULLS LAST],
         column2 [ASC | DESC];

Data Engineering Example

sql
SELECT pipeline_name, run_date, duration_sec
FROM pipeline_runs
ORDER BY pipeline_name ASC, duration_sec DESC;

Expected output:

pipeline_namerun_dateduration_sec
etl_orders2024-01-12600
etl_orders2024-01-10120
etl_orders2024-01-1145
etl_users2024-01-11300

WARNING

Gotcha: Without an ORDER BY, repeated runs of the same query can return rows in different orders — this causes non-deterministic diffs in data validation tests and snapshot-based pipelines. Always add an explicit ORDER BY when the downstream consumer depends on row order.


LIMIT / OFFSET ​

LIMIT restricts the number of rows returned; OFFSET skips a specified number of rows before starting to return results. Together they enable pagination, but they have important performance and correctness implications.

sql
-- ANSI / PostgreSQL / MySQL / SQLite
SELECT columns
FROM table_name
ORDER BY sort_col
LIMIT n OFFSET m;

-- SQL Server / Azure Synapse
SELECT columns
FROM table_name
ORDER BY sort_col
OFFSET m ROWS FETCH NEXT n ROWS ONLY;

Data Engineering Example

Source table: pipeline_runs (from above)

sql
-- Get page 2 (rows 3-4) of the longest pipeline runs
SELECT run_id, pipeline_name, duration_sec
FROM pipeline_runs
ORDER BY duration_sec DESC
LIMIT 2 OFFSET 2;

Expected output:

run_idpipeline_nameduration_sec
1etl_orders120
2etl_orders45

WARNING

Gotcha: LIMIT without ORDER BY is non-deterministic — the engine picks any rows it likes. In data pipelines, using LIMIT for sampling without a deterministic sort produces unreproducible results. Also, large OFFSET values are expensive because the engine must scan and discard rows — prefer keyset (cursor-based) pagination for production pipelines.


2. Joins & Join Traps ​

INNER JOIN ​

An INNER JOIN returns only rows where the join condition is satisfied in both tables. Rows in either table that have no match are excluded from the result.

sql
SELECT a.col, b.col
FROM table_a AS a
INNER JOIN table_b AS b
    ON a.key = b.key;

Data Engineering Example

orders

order_idcustomer_idamount
1101250.00
210275.00
399930.00

customers

customer_idname
101Alice
102Bob
103Charlie
sql
SELECT o.order_id, c.name, o.amount
FROM orders AS o
INNER JOIN customers AS c
    ON o.customer_id = c.customer_id;

Expected output:

order_idnameamount
1Alice250.00
2Bob75.00

WARNING

Gotcha: If the join key contains NULLs, those rows will never match — NULL = NULL evaluates to UNKNOWN, not TRUE. This silently drops rows in data pipelines. Use IS NOT DISTINCT FROM (or COALESCE) when you need NULL-safe joins.


LEFT JOIN (LEFT OUTER JOIN) ​

A LEFT JOIN returns all rows from the left table and the matching rows from the right table. Where there is no match, the right-side columns are filled with NULLs.

sql
SELECT a.col, b.col
FROM table_a AS a
LEFT JOIN table_b AS b
    ON a.key = b.key;

Data Engineering Example

sql
SELECT o.order_id, c.name, o.amount
FROM orders AS o
LEFT JOIN customers AS c
    ON o.customer_id = c.customer_id;

Expected output:

order_idnameamount
1Alice250.00
2Bob75.00
3NULL30.00

WARNING

Gotcha: Placing a filter on the right table's column in the WHERE clause converts a LEFT JOIN into an INNER JOIN. For example, WHERE c.country = 'US' excludes NULLs and drops all unmatched rows. Move such filters into the ON clause to preserve the LEFT JOIN semantics.


RIGHT JOIN (RIGHT OUTER JOIN) ​

A RIGHT JOIN returns all rows from the right table and matching rows from the left. It is the mirror image of LEFT JOIN. Most teams prefer rewriting RIGHT JOIN as a LEFT JOIN with the tables swapped for readability.

sql
SELECT a.col, b.col
FROM table_a AS a
RIGHT JOIN table_b AS b
    ON a.key = b.key;

Data Engineering Example

sql
SELECT o.order_id, c.name
FROM orders AS o
RIGHT JOIN customers AS c
    ON o.customer_id = c.customer_id;

Expected output:

order_idname
1Alice
2Bob
NULLCharlie

WARNING

Gotcha: RIGHT JOIN is rarely used in practice and can confuse code reviewers. Convention in most data teams is to use LEFT JOIN exclusively by swapping table positions. This consistency makes pipeline SQL easier to audit.


FULL OUTER JOIN ​

A FULL OUTER JOIN returns all rows from both tables. Where there is no match, the missing side is filled with NULLs. It is essential for data reconciliation — comparing two data sources to find rows that exist in one but not the other.

sql
SELECT a.col, b.col
FROM table_a AS a
FULL OUTER JOIN table_b AS b
    ON a.key = b.key;

Data Engineering Example

source_a (CRM extract)

customer_idemail
101alice@example.com
102bob@example.com

source_b (billing extract)

customer_idemail
102bob@example.com
103charlie@co.com
sql
SELECT
    COALESCE(a.customer_id, b.customer_id) AS customer_id,
    a.email AS crm_email,
    b.email AS billing_email,
    CASE
        WHEN a.customer_id IS NULL THEN 'Missing in CRM'
        WHEN b.customer_id IS NULL THEN 'Missing in Billing'
        ELSE 'Matched'
    END AS reconciliation_status
FROM source_a AS a
FULL OUTER JOIN source_b AS b
    ON a.customer_id = b.customer_id;

Expected output:

customer_idcrm_emailbilling_emailreconciliation_status
101alice@example.comNULLMissing in Billing
102bob@example.combob@example.comMatched
103NULLcharlie@co.comMissing in CRM

WARNING

Gotcha: MySQL does not support FULL OUTER JOIN. You must emulate it with a LEFT JOIN + UNION ALL + a filtered RIGHT JOIN (or anti-join). Always verify engine support.


CROSS JOIN ​

A CROSS JOIN produces the Cartesian product of two tables — every row from the left table paired with every row from the right table. It is rarely used accidentally but is powerful for generating date spines, parameter grids, or scaffolding dimension tables.

sql
SELECT a.col, b.col
FROM table_a AS a
CROSS JOIN table_b AS b;

Data Engineering Example

date_spine

report_date
2024-01-01
2024-01-02
2024-01-03

metrics

metric_name
revenue
users
sql
SELECT d.report_date, m.metric_name
FROM date_spine AS d
CROSS JOIN metrics AS m;

Expected output (6 rows):

report_datemetric_name
2024-01-01revenue
2024-01-01users
2024-01-02revenue
2024-01-02users
2024-01-03revenue
2024-01-03users

WARNING

Gotcha: A CROSS JOIN on two large tables produces rows_A × rows_B output rows and can crash your cluster or exhaust memory. In data pipelines, an accidental cross join is usually caused by omitting the ON clause — some engines (e.g., Spark SQL) silently treat JOIN without ON as a CROSS JOIN.


ANTI JOIN ​

An ANTI JOIN returns rows from the left table that have no match in the right table. ANSI SQL has no ANTI JOIN keyword; it is expressed using LEFT JOIN ... WHERE right.key IS NULL or NOT EXISTS.

sql
-- Pattern 1: LEFT JOIN + IS NULL
SELECT a.*
FROM table_a AS a
LEFT JOIN table_b AS b
    ON a.key = b.key
WHERE b.key IS NULL;

-- Pattern 2: NOT EXISTS (often preferred by optimizers)
SELECT a.*
FROM table_a AS a
WHERE NOT EXISTS (
    SELECT 1 FROM table_b AS b WHERE b.key = a.key
);

Data Engineering Example

sql
-- Find orders with no matching customer (orphan records)
SELECT o.*
FROM orders AS o
LEFT JOIN customers AS c
    ON o.customer_id = c.customer_id
WHERE c.customer_id IS NULL;

Expected output:

order_idcustomer_idamount
399930.00

WARNING

Gotcha: When using the LEFT JOIN + IS NULL pattern, make sure the IS NULL check is on a non-nullable column of the right table (typically the join key or the primary key). Checking a column that is legitimately nullable will produce false positives.


SEMI JOIN ​

A SEMI JOIN returns rows from the left table where at least one match exists in the right table, but does not duplicate rows when multiple matches exist (unlike INNER JOIN). Like ANTI JOIN, there is no SEMI JOIN keyword in ANSI SQL.

sql
-- Pattern: EXISTS
SELECT a.*
FROM table_a AS a
WHERE EXISTS (
    SELECT 1 FROM table_b AS b WHERE b.key = a.key
);

-- Pattern: IN (simpler but beware NULLs)
SELECT a.*
FROM table_a AS a
WHERE a.key IN (SELECT b.key FROM table_b);

Data Engineering Example

users

user_idname
101Alice
102Bob
103Charlie

login_events (many logins per user)

event_iduser_idlogin_at
11012024-01-15 08:00:00
21012024-01-15 09:00:00
31022024-01-15 10:00:00
sql
-- Users who logged in at least once (no duplicates)
SELECT u.*
FROM users AS u
WHERE EXISTS (
    SELECT 1 FROM login_events AS le WHERE le.user_id = u.user_id
);

Expected output:

user_idname
101Alice
102Bob

WARNING

Gotcha: The IN subquery pattern breaks when the subquery returns NULLs. x IN (1, 2, NULL) where x = 3 evaluates to UNKNOWN (not FALSE), and wrapping it with NOT IN returns zero rows. Prefer EXISTS / NOT EXISTS for NULL safety.


Fan-Out & Cartesian Explosion Diagnosis ​

A fan-out (Cartesian explosion) occurs when a join produces more rows than expected because the join key is not unique on one or both sides. This is the #1 cause of incorrect aggregate values in data pipelines.

Diagnosis Pattern

sql
-- Step 1: Check if the join key is unique
SELECT join_key, COUNT(*) AS cnt
FROM table_b
GROUP BY join_key
HAVING COUNT(*) > 1;

-- Step 2: Compare row counts before and after join
SELECT
    (SELECT COUNT(*) FROM table_a) AS rows_before,
    (SELECT COUNT(*) FROM table_a JOIN table_b USING (key)) AS rows_after;
-- If rows_after > rows_before, you have a fan-out.

Data Engineering Example

orders

order_idproduct_id
1P1
2P2

promotions (multiple promos per product)

promo_idproduct_iddiscount
AP110
BP120
CP25
sql
-- Dangerous: fan-out doubles order 1
SELECT o.order_id, p.discount
FROM orders AS o
INNER JOIN promotions AS p
    ON o.product_id = p.product_id;

Result (3 rows from 2 orders — fan-out!):

order_iddiscount
110
120
25

WARNING

Gotcha: Fan-outs silently inflate SUM, COUNT, and other aggregates. Before joining, always verify the grain (uniqueness) of each table's join key. Use COUNT(*) ... GROUP BY key HAVING COUNT(*) > 1 to check. In dbt, use the unique test on your models' primary keys.


3. Set Operations ​

UNION vs UNION ALL ​

UNION ALL concatenates two result sets preserving all rows (including duplicates). UNION (without ALL) performs UNION ALL followed by an implicit DISTINCT, which adds a sort or hash step.

sql
SELECT columns FROM table_a
UNION ALL              -- keeps duplicates, faster
SELECT columns FROM table_b;

SELECT columns FROM table_a
UNION                  -- removes duplicates, slower
SELECT columns FROM table_b;

Data Engineering Example

events_2024_q1

user_idevent_type
101click
102view

events_2024_q2

user_idevent_type
101click
103signup
sql
-- Combine quarterly partitions (preserving duplicates with UNION ALL)
SELECT user_id, event_type FROM events_2024_q1
UNION ALL
SELECT user_id, event_type FROM events_2024_q2;

Expected output (UNION ALL — 4 rows):

user_idevent_type
101click
102view
101click
103signup
sql
-- Combine and deduplicate with UNION (removes the duplicate 101 / click row)
SELECT user_id, event_type FROM events_2024_q1
UNION
SELECT user_id, event_type FROM events_2024_q2;

Expected output (UNION — 3 rows):

user_idevent_type
101click
102view
103signup

WARNING

Gotcha: In data pipelines, UNION (without ALL) silently removes legitimate duplicate events. The dedup sort is also expensive on large tables. Default to UNION ALL unless you specifically need deduplication — and if you do, make it explicit with a DISTINCT on specific columns so intent is clear.


INTERSECT ​

INTERSECT returns only the rows that appear in both result sets. Like UNION, it implicitly deduplicates.

sql
SELECT columns FROM table_a
INTERSECT
SELECT columns FROM table_b;

Data Engineering Example

sql
-- Users active in both quarters
SELECT user_id FROM events_2024_q1
INTERSECT
SELECT user_id FROM events_2024_q2;

Expected output:

user_id
101

WARNING

Gotcha: INTERSECT treats NULLs as equal for comparison purposes (unlike =). Two rows (1, NULL) and (1, NULL) will match. This is convenient but inconsistent with how NULLs behave in JOIN or WHERE conditions — be aware of this semantic difference.


EXCEPT / MINUS ​

EXCEPT (called MINUS in Oracle) returns rows from the first query that are not present in the second query. It is the set-operation counterpart of the ANTI JOIN.

sql
SELECT columns FROM table_a
EXCEPT                 -- Oracle: MINUS
SELECT columns FROM table_b;

Data Engineering Example

sql
-- Users who were active in Q1 but churned (not in Q2)
SELECT user_id FROM events_2024_q1
EXCEPT
SELECT user_id FROM events_2024_q2;

Expected output:

user_id
102

WARNING

Gotcha: The column count and data types must match across all set operations. A mismatch in column order (e.g., swapping user_id and event_date between the two SELECTs) will either error or silently compare wrong columns, producing garbage results.


4. NULL Semantics ​

Three-Valued Logic ​

SQL uses three-valued logic: every comparison evaluates to TRUE, FALSE, or UNKNOWN. Any operation involving NULL yields UNKNOWN. WHERE and HAVING clauses only include rows where the condition is TRUE — rows with UNKNOWN are excluded, just like FALSE.

sql
-- All of these evaluate to UNKNOWN (not TRUE, not FALSE):
NULL = NULL        -- UNKNOWN
NULL <> 1          -- UNKNOWN
NULL > 0           -- UNKNOWN
NULL AND TRUE      -- UNKNOWN
NOT NULL           -- UNKNOWN

Data Engineering Example

sensor_readings

sensor_idtemperature
S122.5
S2NULL
S318.0
sql
-- This will NOT return S2 — NULL <> 20 is UNKNOWN
SELECT * FROM sensor_readings WHERE temperature <> 20;

Expected output (S2 is excluded):

sensor_idtemperature
S122.5
S318.0
sql
-- To include S2, explicitly handle NULL:
SELECT * FROM sensor_readings
WHERE temperature <> 20 OR temperature IS NULL;

WARNING

Gotcha: NOT IN with NULLs is the classic trap. WHERE x NOT IN (1, 2, NULL) returns zero rows for every value of x, because x <> NULL is UNKNOWN, making the entire expression UNKNOWN. Use NOT EXISTS instead.


COALESCE ​

COALESCE returns the first non-NULL value from a list of expressions. It is ANSI SQL and works on all engines. It is essential for providing defaults, merging columns from different sources, and handling optional fields.

sql
COALESCE(expr1, expr2, ..., exprN)

Data Engineering Example

user_profiles

user_idpreferred_namefull_nameemail
1AlAlice Smithalice@co.com
2NULLBob Jonesbob@co.com
3NULLNULLcharlie@co.com
sql
SELECT
    user_id,
    COALESCE(preferred_name, full_name, email) AS display_name
FROM user_profiles;

Expected output:

user_iddisplay_name
1Al
2Bob Jones
3charlie@co.com

WARNING

Gotcha: COALESCE evaluates arguments left to right and returns the first non-NULL value. If all arguments are NULL, the result is NULL — not an empty string. To guarantee a non-NULL result, always include a literal default as the last argument: COALESCE(col1, col2, 'unknown').


NULLIF ​

NULLIF(a, b) returns NULL if a = b, otherwise returns a. It is commonly used to convert sentinel values (like empty strings or zeros) into proper NULLs for correct aggregate calculations.

sql
NULLIF(expression, value_to_nullify)

Data Engineering Example

legacy_records

record_idstatus
1active
2
3inactive
sql
-- Convert empty strings (common in CSV imports) to NULL
SELECT
    record_id,
    NULLIF(status, '') AS clean_status
FROM legacy_records;

Expected output:

record_idclean_status
1active
2NULL
3inactive

WARNING

Gotcha: NULLIF is asymmetric — NULLIF(a, b) returns NULL when a = b, but NULLIF(b, a) returns NULL when b = a. This matters when the arguments are different types; implicit casting may produce unexpected results. Also, do not confuse NULLIF with IFNULL/NVL — they do the opposite thing.


NVL / IFNULL ​

NVL(expr, default) (Oracle) and IFNULL(expr, default) (MySQL, SQLite) are two-argument shortcuts equivalent to COALESCE(expr, default). They return default if expr is NULL.

sql
-- Oracle
NVL(column, default_value)

-- MySQL / SQLite
IFNULL(column, default_value)

-- ANSI equivalent (preferred for portability)
COALESCE(column, default_value)

Data Engineering Example

shipments

order_idshipping_cost
112.50
2NULL
38.00
sql
-- Replace NULL shipping costs with 0 (portable ANSI COALESCE; Oracle NVL(shipping_cost, 0))
SELECT
    order_id,
    COALESCE(shipping_cost, 0) AS shipping_cost
FROM shipments;

Expected output:

order_idshipping_cost
112.50
20.00
38.00

WARNING

Gotcha: NVL and IFNULL are not ANSI SQL — they are vendor-specific. If your pipeline runs on Snowflake, BigQuery, or PostgreSQL, use COALESCE for portability. Also, NVL in Oracle always evaluates both arguments (unlike COALESCE which short-circuits), so avoid expensive expressions in the second argument.


IS DISTINCT FROM ​

IS DISTINCT FROM is a NULL-safe equality comparison. It treats NULLs as equal to each other and returns a definitive TRUE or FALSE — never UNKNOWN. Its negation is IS NOT DISTINCT FROM.

sql
-- Returns TRUE/FALSE, never UNKNOWN
a IS DISTINCT FROM b
a IS NOT DISTINCT FROM b

Data Engineering Example

snapshot_old

user_idstatus
1active
2NULL

snapshot_new

user_idstatus
1active
2NULL
sql
-- Detect actual changes between snapshots (NULL-safe)
SELECT o.user_id
FROM snapshot_old AS o
JOIN snapshot_new AS n ON o.user_id = n.user_id
WHERE o.status IS DISTINCT FROM n.status;

Expected output: (empty — both snapshots are the same, even the NULLs)

user_id
(no rows)

WARNING

Gotcha: Not all engines support IS DISTINCT FROM. MySQL requires <=> (the NULL-safe equality operator) and you negate it with NOT (a <=> b). BigQuery supports it as IS DISTINCT FROM. Always check your engine's documentation.


5. Type Casting & Coercion ​

CAST ​

CAST explicitly converts a value from one data type to another. It is ANSI SQL and supported by all major engines. It fails at runtime if the conversion is impossible.

sql
CAST(expression AS target_type)

-- Shorthand (PostgreSQL)
expression::target_type

Data Engineering Example

raw_ingestion

record_idamount_strevent_date_str
11500.502024-01-15
2299.992024-02-20
sql
SELECT
    record_id,
    CAST(amount_str AS DECIMAL(10, 2))   AS amount,
    CAST(event_date_str AS DATE)         AS event_date
FROM raw_ingestion;

Expected output:

record_idamountevent_date
11500.502024-01-15
2299.992024-02-20

WARNING

Gotcha: CAST is strict — if a value cannot be converted (e.g., CAST('abc' AS INTEGER)), the query fails with an error, which can crash your entire pipeline run. For raw/staging data that may contain malformed values, use TRY_CAST or SAFE_CAST instead.


TRY_CAST / SAFE_CAST ​

TRY_CAST (SQL Server, Snowflake, Databricks) and SAFE_CAST (BigQuery) attempt the conversion but return NULL instead of raising an error when the cast fails. This is critical for data pipelines processing dirty or unvalidated source data.

sql
-- SQL Server / Snowflake / Databricks
TRY_CAST(expression AS target_type)

-- BigQuery
SAFE_CAST(expression AS target_type)

-- PostgreSQL (no built-in TRY_CAST — use a custom function or regex guard)

Data Engineering Example

raw_csv_import

row_numrevenue_str
11200.00
2N/A
3850.75
4-
sql
SELECT
    row_num,
    revenue_str,
    TRY_CAST(revenue_str AS DECIMAL(10, 2)) AS revenue_clean
FROM raw_csv_import;

Expected output:

row_numrevenue_strrevenue_clean
11200.001200.00
2N/ANULL
3850.75850.75
4-NULL

WARNING

Gotcha: TRY_CAST silently turns bad data into NULLs, which can mask data quality issues. In production pipelines, combine it with a data quality check: SELECT COUNT(*) FROM raw WHERE TRY_CAST(col AS INT) IS NULL AND col IS NOT NULL — this tells you how many rows had unparseable values, so you can alert on regressions.


Safe Parsing Patterns ​

When TRY_CAST is not available or when you need more control over parsing (e.g., date formats, regex validation), use safe parsing patterns that combine conditional logic with CAST.

sql
-- Guard with a regex or CASE before casting
SELECT
    CASE
        WHEN value ~ '^\d+$'                  -- PostgreSQL regex
        THEN CAST(value AS INTEGER)
        ELSE NULL
    END AS parsed_int
FROM raw_data;

-- BigQuery: SAFE.PARSE_DATE for date strings
SAFE.PARSE_DATE('%Y-%m-%d', date_string)

-- Snowflake: TRY_TO_DATE / TRY_TO_NUMBER
TRY_TO_DATE(date_string, 'YYYY-MM-DD')
TRY_TO_NUMBER(numeric_string, 10, 2)

Data Engineering Example

api_responses

response_idtimestamp_str
12024-01-15T08:30:00Z
2invalid_ts
32024-01-16T12:00:00Z
sql
-- Guard with format validation before CAST (ANSI-portable pattern)
SELECT
    response_id,
    CASE
        WHEN timestamp_str LIKE '____-__-__T__:__:__Z'
        THEN CAST(SUBSTRING(timestamp_str, 1, 10) AS DATE)
        ELSE NULL
    END AS parsed_date
FROM api_responses;

Expected output:

response_idparsed_date
12024-01-15
2NULL
32024-01-16

WARNING

Gotcha: Date/time parsing is the most common source of silent data corruption in pipelines. A format mismatch can swap month and day (01/02/2024 → Jan 2 or Feb 1?) without any error. Always specify the format explicitly and never rely on implicit parsing. Add data quality assertions that check for NULL parse results in your staging layer.


End of Module 1