⭐ Flagship Reference Handbook⏱️ 15 min read • 14 Production Recipes

The Ultimate SQL Patterns Cheatsheet
From Foundations to Advanced Mastery

A practical, problem-first reference manual for analytical engineers, data analysts, and software developers. Whether you're preparing for data engineering interviews or debugging complex window logic in production, use this handbook to navigate execution order, gaps & islands, sessionization, recursive CTEs, and optimization traps.

🦆Running queries on DuckDB? Check out thePostgres vs DuckDB Dialect Guide →
Practice on Live Datasets
Foundations

1. The SQL Logical Execution Pipeline

Why WHERE cannot see column aliases and why HAVING filters after aggregation.

The Problem & Mental Model

In SQL, the code is written in lexical order (SELECT → FROM → WHERE), but the query engine executes it in an entirely different logical sequence. Attempting to filter by a newly aliased column in WHERE causes an 'Unknown Column' error.

❌ Syntax Error in WHERE
-- Fails: WHERE executes BEFORE SELECT, so alias 'net_rev' does not exist yet!
SELECT 
  product_id,
  gross_sales - discount_amount AS net_rev
FROM sales
WHERE net_rev > 1000;
✅ Correct: CTE, Subquery, or Direct Expression
-- Method A: Repeat the underlying expression
SELECT 
  product_id,
  gross_sales - discount_amount AS net_rev
FROM sales
WHERE (gross_sales - discount_amount) > 1000;

-- Method B: Encapsulate with a CTE (Clean & Readable)
WITH calculated_sales AS (
  SELECT 
    product_id,
    gross_sales - discount_amount AS net_rev
  FROM sales
)
SELECT * 
FROM calculated_sales 
WHERE net_rev > 1000;
Key Takeaway: Logical processing sequence: 1. FROM & JOIN → 2. WHERE → 3. GROUP BY → 4. HAVING → 5. WINDOW → 6. SELECT → 7. DISTINCT → 8. QUALIFY → 9. ORDER BY → 10. LIMIT.
💡Engine Note: DuckDB and Snowflake support QUALIFY to filter directly on window functions without needing an extra subquery.
Foundations

2. The 3-Valued Logic & NOT IN (NULL) Trap

How NULL comparisons silently evaluate to UNKNOWN and wipe out query results.

The Problem & Mental Model

In SQL, comparisons with NULL yield UNKNOWN (neither TRUE nor FALSE). If a subquery used with NOT IN returns even a single NULL value, the entire NOT IN condition evaluates to UNKNOWN, returning zero rows.

⚠️ The Fatal NOT IN with NULL Trap
-- If department.manager_id contains even ONE NULL, 
-- this query returns 0 rows, even if matching employees exist!
SELECT employee_name 
FROM employees 
WHERE employee_id NOT IN (
  SELECT manager_id FROM departments
);
✅ Robust Pattern: NOT EXISTS or IS NOT NULL
-- Approach 1: NOT EXISTS (Null-safe, optimizer-friendly)
SELECT e.employee_name
FROM employees e
WHERE NOT EXISTS (
  SELECT 1 
  FROM departments d 
  WHERE d.manager_id = e.employee_id
);

-- Approach 2: Explicit NULL filter inside NOT IN
SELECT employee_name
FROM employees
WHERE employee_id NOT IN (
  SELECT manager_id 
  FROM departments 
  WHERE manager_id IS NOT NULL
);
Key Takeaway: Because NOT (x IN (1, NULL)) translates to (x != 1 AND x != NULL), and (x != NULL) is UNKNOWN, the whole expression becomes UNKNOWN. Always prefer NOT EXISTS or ensure NULLs are excluded.
💡Engine Note: DuckDB and Postgres optimizers easily turn NOT EXISTS into efficient Anti-Hash Joins.
Foundations

3. Outer Join Filtering: ON vs. WHERE

Accidentally turning a LEFT JOIN into an INNER JOIN by placing right-table predicates in WHERE.

The Problem & Mental Model

When using LEFT JOIN, putting a condition on the right table inside WHERE filters out the NULL-padded rows created for non-matching left records, completely destroying the outer join.

❌ Accidental INNER JOIN Bug
-- Drops customers with NO orders because o.status IS NULL is filtered out!
SELECT 
  c.customer_name, 
  o.order_id, 
  o.amount
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
WHERE o.status = 'COMPLETED';
✅ Correct: Filter in ON or Use COALESCE
-- Approach 1: Put right-table filter directly in the ON clause
SELECT 
  c.customer_name, 
  o.order_id, 
  o.amount
FROM customers c
LEFT JOIN orders o 
  ON c.customer_id = o.customer_id 
 AND o.status = 'COMPLETED';

-- Approach 2: If filtering in WHERE, explicitly account for NULLs
SELECT 
  c.customer_name, 
  o.order_id, 
  o.amount
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id
WHERE o.order_id IS NULL OR o.status = 'COMPLETED';
Key Takeaway: The ON clause dictates what rows match during the join. The WHERE clause filters rows AFTER the join has produced its candidate table.
Date & Time

4. Date Truncation, Interval Arithmetic & Moving Periods

Standardizing time grains for Month-over-Month and Rolling Time Windows.

The Problem & Mental Model

Real-world data comes with timestamps. To aggregate by month, week, or compute rolling 30-day windows, you must truncate dates and manipulate intervals cleanly without string slicing.

✅ Standard SQL & DuckDB Date Wrangling
-- 1. Truncate to Month / Week grain
SELECT 
  date_trunc('month', order_date) AS order_month,
  date_trunc('week', order_date)  AS order_week,
  SUM(amount) AS monthly_revenue
FROM orders
GROUP BY 1, 2;

-- 2. Interval Arithmetic (Adding / Subtracting Time)
SELECT 
  order_id,
  order_date,
  order_date + INTERVAL 30 DAY   AS warranty_expiry,
  order_date - INTERVAL 1 MONTH  AS previous_month_benchmark
FROM orders;

-- 3. Calculate Date Difference in Days
SELECT 
  customer_id,
  date_diff('day', signup_date, first_purchase_date) AS days_to_activate
FROM user_lifecycle;
Key Takeaway: DATE_TRUNC preserves the timestamp datatype while resetting sub-grains to zero. Interval arithmetic (INTERVAL n UNIT) is ANSI standard and highly legible.
💡Engine Note: In DuckDB, date_diff('day', start, end) or simple subtraction (end - start) gives exact elapsed day counts.
Window Functions

5. Window Ranking Functions: ROW_NUMBER vs RANK vs DENSE_RANK

How to choose the correct ranking function when dealing with duplicate tie values.

The Problem & Mental Model

Different business questions require different tie-breaking semantics. For pagination you need unique numbers; for Olympic medals you need gaps; for top salary tiers you need dense ranks.

✅ Comparative Ranking Behavior
SELECT 
  student_name,
  score,
  -- 1, 2, 3, 4 (Always unique, arbitrary tie breaker if unconstrained)
  ROW_NUMBER() OVER (ORDER BY score DESC) AS row_num,
  
  -- 1, 2, 2, 4 (Leaves gaps after ties — classic competition rank)
  RANK()       OVER (ORDER BY score DESC) AS rank_pos,
  
  -- 1, 2, 2, 3 (No gaps — dense rank)
  DENSE_RANK() OVER (ORDER BY score DESC) AS dense_pos,
  
  -- 1, 1, 2, 2 (Divides rows into N roughly equal quartiles/buckets)
  NTILE(2)     OVER (ORDER BY score DESC) AS quartile
FROM exam_results;
Key Takeaway: If scores are [100, 90, 90, 80]: • ROW_NUMBER produces: 1, 2, 3, 4 • RANK produces: 1, 2, 2, 4 • DENSE_RANK produces: 1, 2, 2, 3 • NTILE(2) produces: 1, 1, 2, 2.
Window Functions

6. Window Frames & Rolling Moving Averages

Controlling the sliding frame with ROWS BETWEEN to compute rolling 7-day sums and centered averages.

The Problem & Mental Model

By default, an OVER (ORDER BY ...) clause uses RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, which calculates cumulative totals and bundles duplicate values. Moving windows require explicit frame bounds.

✅ Rolling 7-Day & Centered Moving Averages
SELECT 
  recorded_date,
  daily_revenue,
  
  -- Cumulative YTD Total (from start of partition to today)
  SUM(daily_revenue) OVER (
    ORDER BY recorded_date 
    ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
  ) AS cumulative_revenue,

  -- 7-Day Rolling Moving Average (Today + Past 6 Days = 7 rows)
  AVG(daily_revenue) OVER (
    ORDER BY recorded_date 
    ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
  ) AS rolling_7d_avg,

  -- Centered 3-Day Window (Yesterday + Today + Tomorrow)
  AVG(daily_revenue) OVER (
    ORDER BY recorded_date 
    ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING
  ) AS centered_3d_avg
FROM daily_metrics;
Key Takeaway: ROWS specifies physical row offsets. RANGE specifies logical value offsets. Always use ROWS BETWEEN for deterministic rolling metrics.
Advanced Patterns

7. The Gaps & Islands Pattern (Consecutive Streaks)

Identify consecutive login streaks, unbroken uptime periods, and contiguous event blocks.

The Problem & Mental Model

You need to find users who logged in for 3 or more consecutive days, or find continuous time spans where a server was online without downtime.

✅ The Row_Number Difference Method
WITH ordered_logins AS (
  -- Step 1: Deduplicate distinct login dates per user
  SELECT DISTINCT 
    user_id, 
    CAST(login_time AS DATE) AS login_date
  FROM user_logins
),
numbered_logins AS (
  -- Step 2: Subtract ROW_NUMBER() from login_date to create an island group key
  SELECT 
    user_id,
    login_date,
    ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_date) AS rn,
    login_date - (ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_date) * INTERVAL 1 DAY) AS island_id
  FROM ordered_logins
)
-- Step 3: Group by user and island_id to measure streak length
SELECT 
  user_id,
  MIN(login_date) AS streak_start,
  MAX(login_date) AS streak_end,
  COUNT(*) AS consecutive_days
FROM numbered_logins
GROUP BY user_id, island_id
HAVING COUNT(*) >= 3
ORDER BY user_id, streak_start;
Key Takeaway: The intuition: As long as dates increment by exactly 1 day alongside ROW_NUMBER, the mathematical difference (login_date - rn days) remains constant. When a gap occurs, the difference shifts to a new island identifier!
Advanced Patterns

8. Sessionization Pattern (Inactivity Timeout Windows)

Group continuous clickstreams into user sessions separated by 30 minutes of idle time.

The Problem & Mental Model

Clickstream logs are raw event streams without session IDs. A session ends whenever a user remains inactive for more than 30 minutes.

✅ LAG + Cumulative SUM Sessionization
WITH event_deltas AS (
  -- Step 1: Find previous event timestamp per user
  SELECT 
    user_id,
    event_time,
    page_url,
    LAG(event_time) OVER (
      PARTITION BY user_id 
      ORDER BY event_time
    ) AS prev_event_time
  FROM clickstream_events
),
session_flags AS (
  -- Step 2: Flag 1 if gap > 30 minutes or first event, else 0
  SELECT 
    user_id,
    event_time,
    page_url,
    CASE 
      WHEN prev_event_time IS NULL 
        OR date_diff('minute', prev_event_time, event_time) > 30 THEN 1 
      ELSE 0 
    END AS is_new_session
  FROM event_deltas
),
session_assigned AS (
  -- Step 3: Cumulative sum of flags forms unique session IDs
  SELECT 
    user_id,
    event_time,
    page_url,
    SUM(is_new_session) OVER (
      PARTITION BY user_id 
      ORDER BY event_time
      ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS session_number
  FROM session_flags
)
SELECT 
  user_id,
  session_number,
  MIN(event_time) AS session_start,
  MAX(event_time) AS session_end,
  COUNT(*) AS total_pageviews
FROM session_assigned
GROUP BY user_id, session_number;
Key Takeaway: Cumulative sum of boolean flags is one of the most versatile techniques in analytical SQL. Each time is_new_session = 1, the running sum increments, labeling all subsequent events with the new session ID.
Advanced Patterns

9. Top-N Per Group & Deduplication (with QUALIFY)

Extract the latest or highest record per entity using window rank CTEs and DuckDB QUALIFY.

The Problem & Mental Model

Given an orders table with multiple orders per customer, return only the single most recent order per customer.

✅ Standard CTE vs DuckDB QUALIFY
-- Method 1: Standard ANSI SQL (Works everywhere: Postgres, BigQuery, Snowflake)
WITH ranked_orders AS (
  SELECT 
    order_id,
    customer_id,
    order_date,
    amount,
    ROW_NUMBER() OVER (
      PARTITION BY customer_id 
      ORDER BY order_date DESC, order_id DESC
    ) AS rn
  FROM orders
)
SELECT order_id, customer_id, order_date, amount
FROM ranked_orders
WHERE rn = 1;

-- Method 2: Modern DuckDB / Snowflake QUALIFY Clause (No CTE required!)
SELECT 
  order_id,
  customer_id,
  order_date,
  amount
FROM orders
QUALIFY ROW_NUMBER() OVER (
  PARTITION BY customer_id 
  ORDER BY order_date DESC, order_id DESC
) = 1;
Key Takeaway: The QUALIFY clause acts like a HAVING clause, but specifically for window functions. It eliminates boilerplate CTEs for deduplication and Top-N queries.
💡Engine Note: KaizenCodes supports DuckDB's QUALIFY syntax out-of-the-box in all practice challenges.
Advanced Patterns

10. Anti-Joins: Finding Non-Existent Relationships

Identify records present in Dataset A that have zero occurrences in Dataset B.

The Problem & Mental Model

Find all customers who registered in 2023 but never placed a single purchase in 2024.

✅ LEFT JOIN IS NULL vs NOT EXISTS vs EXCEPT
-- Approach 1: LEFT JOIN with IS NULL check (High performance on indexed keys)
SELECT c.customer_id, c.customer_name
FROM customers c
LEFT JOIN orders o 
  ON c.customer_id = o.customer_id 
 AND o.order_date >= '2024-01-01'
WHERE c.signup_date BETWEEN '2023-01-01' AND '2023-12-31'
  AND o.order_id IS NULL;

-- Approach 2: NOT EXISTS (Clear correlated subquery intent)
SELECT c.customer_id, c.customer_name
FROM customers c
WHERE c.signup_date BETWEEN '2023-01-01' AND '2023-12-31'
  AND NOT EXISTS (
    SELECT 1 
    FROM orders o 
    WHERE o.customer_id = c.customer_id 
      AND o.order_date >= '2024-01-01'
  );
Key Takeaway: Both patterns produce identical plans in modern query optimizers. Avoid NOT IN when subquery columns might contain NULLs.
Advanced Patterns

11. Pivoting & Unpivoting (Long to Wide & Wide to Long)

Reshaping dimensional tabular data for cross-tab reports and analytical pipelines.

The Problem & Mental Model

Convert rows containing quarterly sales into separate Q1, Q2, Q3, Q4 columns (pivoting), or melt wide columns back into rows (unpivoting).

✅ Conditional Aggregation Pivot & UNPIVOT
-- 1. Pivoting: Long to Wide using Conditional Aggregation
SELECT 
  product_id,
  SUM(CASE WHEN quarter = 'Q1' THEN sales_amount ELSE 0 END) AS q1_sales,
  SUM(CASE WHEN quarter = 'Q2' THEN sales_amount ELSE 0 END) AS q2_sales,
  SUM(CASE WHEN quarter = 'Q3' THEN sales_amount ELSE 0 END) AS q3_sales,
  SUM(CASE WHEN quarter = 'Q4' THEN sales_amount ELSE 0 END) AS q4_sales
FROM quarterly_sales
GROUP BY product_id;

-- 2. Modern DuckDB PIVOT Syntax
PIVOT quarterly_sales 
ON quarter 
USING SUM(sales_amount) 
GROUP BY product_id;

-- 3. Unpivoting: Wide to Long using UNION ALL or UNPIVOT
SELECT product_id, 'Q1' AS quarter, q1_sales AS sales FROM wide_sales
UNION ALL
SELECT product_id, 'Q2' AS quarter, q2_sales AS sales FROM wide_sales
UNION ALL
SELECT product_id, 'Q3' AS quarter, q3_sales AS sales FROM wide_sales;
Key Takeaway: Conditional aggregation (SUM/MAX with CASE WHEN) is 100% portable across every SQL engine in existence.
Advanced Patterns

12. Recursive Common Table Expressions (Hierarchies & Trees)

Traversing manager-employee organizational trees and graph path relationships.

The Problem & Mental Model

Given an employee table where each row has a manager_id pointing to another employee, build the full reporting hierarchy with employee depth levels.

✅ Recursive CTE Org Hierarchy Traversal
WITH RECURSIVE org_hierarchy AS (
  -- Anchor Member: Find the CEO / Top-level roots (manager_id IS NULL)
  SELECT 
    employee_id,
    employee_name,
    manager_id,
    1 AS hierarchy_level,
    CAST(employee_name AS VARCHAR) AS management_path
  FROM employees
  WHERE manager_id IS NULL

  UNION ALL

  -- Recursive Member: Join employees to their managers in the hierarchy
  SELECT 
    e.employee_id,
    e.employee_name,
    e.manager_id,
    h.hierarchy_level + 1,
    h.management_path || ' -> ' || e.employee_name
  FROM employees e
  INNER JOIN org_hierarchy h ON e.manager_id = h.employee_id
)
SELECT 
  employee_id,
  employee_name,
  hierarchy_level,
  management_path
FROM org_hierarchy
ORDER BY hierarchy_level, employee_id;
Key Takeaway: A recursive CTE consists of: 1) Anchor Query (base case), 2) UNION ALL, and 3) Recursive Query referencing the CTE itself. It repeats until the join returns no new rows.
Advanced Patterns

13. Semi-Structured Data: Arrays, UNNEST & JSON Extraction

Exploding array elements and parsing nested JSON properties in modern analytical engines.

The Problem & Mental Model

Data pipelines frequently store tags as arrays (['sql', 'python']) or webhook payloads as raw JSON strings.

✅ Array Unnesting & JSON Parsing
-- 1. Unnesting Arrays to Rows
SELECT 
  article_id,
  tag_element
FROM blog_posts,
UNNEST(tags_array) AS t(tag_element);

-- 2. DuckDB / Postgres JSON extraction
SELECT 
  event_id,
  -- Extract raw text value
  raw_payload->>'user_id' AS user_id,
  -- Extract nested JSON object or int
  CAST(raw_payload->'device'->>'screen_width' AS INTEGER) AS screen_width
FROM raw_logs;
Key Takeaway: UNNEST expands a list/array column into multiple rows (1-to-many relationship). The ->> operator extracts a JSON field as text.
Optimization

14. Query Optimization: SARGable Predicates & Anti-Patterns

Write index-friendly queries that allow the engine's query optimizer to push down filters.

The Problem & Mental Model

Wrapping filtered columns in scalar functions (like YEAR(col) = 2024 or LOWER(col) = 'abc') prevents the engine from using indexes or partition pruning, forcing a slow full table scan.

❌ Non-SARGable (Forces Full Table Scan)
-- Optimizer cannot push down range predicates through scalar functions!
SELECT * 
FROM transactions
WHERE EXTRACT(YEAR FROM transaction_date) = 2024
  AND LOWER(country_code) = 'us';
✅ SARGable (Search Argument Able) Optimization
-- Optimizer uses partition pruning & binary range scans!
SELECT * 
FROM transactions
WHERE transaction_date >= '2024-01-01' 
  AND transaction_date < '2025-01-01'
  AND country_code IN ('US', 'us');
Key Takeaway: A predicate is SARGable if the query engine can directly leverage indexes or min/max metadata in Parquet file statistics. Keep the column isolated on one side of the operator.

Ready to test your mastery?

Try our 40+ curated DuckDB analytical SQL questions. Instant AST feedback, sub-50ms execution, and our AI Sensei.