Mastering SQL Window Functions: A Beginner's Guide

Vishad PatelVishad Patel
19 min read

Introduction

SQL window functions are powerful tools for performing complex calculations across a set of table rows that are related to the current row. Unlike regular aggregate functions, which collapse rows into a single output, window functions maintain the individual rows while adding an additional layer of calculation. This makes them ideal for tasks such as running totals, moving averages, and ranking operations within specific partitions of data. Understanding and utilizing window functions can greatly enhance your SQL querying capabilities, making it easier to derive meaningful insights from your data.

Explanation of SQL window functions

SQL window functions are a specialized set of functions that perform calculations across a set of table rows that are somehow related to the current row. They do not collapse the result set like aggregate functions, instead, they return a value for each row in the result set. This unique feature allows for sophisticated data analysis directly within SQL queries. Here’s a detailed explanation of how window functions work:

  1. Window Definition: A window function operates over a defined subset of rows, called a window. The window is specified using the OVER() clause, which can include PARTITION BY and ORDER BY clauses. The PARTITION BY clause divides the result set into partitions to which the window function is applied independently, while the ORDER BY clause defines the logical order of rows within each partition.

     sqlCopy codeSELECT 
         employee_id,
         salary,
         AVG(salary) OVER (PARTITION BY department_id) AS avg_department_salary
     FROM employees;
    
  2. Types of Window Functions: There are several types of window functions, each serving different purposes:

    • Aggregate Functions: Common aggregate functions like SUM(), AVG(), MAX(), MIN(), and COUNT() can be used as window functions.

    • Ranking Functions: Functions like ROW_NUMBER(), RANK(), and DENSE_RANK() are used to assign a rank to each row within the partition.

    • Value Functions: Functions like LEAD(), LAG(), FIRST_VALUE(), and LAST_VALUE() provide access to other rows in the result set without using a self-join.

    sqlCopy codeSELECT 
        employee_id,
        salary,
        RANK() OVER (PARTITION BY department_id ORDER BY salary DESC) AS salary_rank
    FROM employees;
  1. Practical Use Cases:

    • Running Totals and Moving Averages: Calculate cumulative totals or moving averages without collapsing the dataset.

        sqlCopy codeSELECT 
            order_id,
            order_date,
            SUM(amount) OVER (ORDER BY order_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_total
        FROM orders;
      
    • Partitioned Analysis: Perform calculations like averages, counts, or sums within specific partitions, such as per department or region.

        sqlCopy codeSELECT 
            product_id,
            sales,
            SUM(sales) OVER (PARTITION BY category_id) AS category_sales
        FROM products;
      
    • Row Navigation: Access values from preceding or following rows to compare current row values with other rows in the dataset.

        sqlCopy codeSELECT 
            order_id,
            order_date,
            LAG(order_date, 1) OVER (ORDER BY order_date) AS previous_order_date
        FROM orders;
      

By leveraging these capabilities, SQL window functions enable more efficient and expressive queries, reducing the need for complex subqueries and joins while enhancing performance and readability. This makes them an invaluable tool for data analysis, reporting, and business intelligence.

Importance of window functions in SQL

  • Non-destructive Analysis: Window functions allow calculations across a set of rows while retaining the original row data, enabling detailed and comprehensive data analysis without losing individual records.

  • Complex Aggregations: They can perform advanced aggregations such as running totals, moving averages, and cumulative sums, providing deeper insights into trends and patterns over a dataset.

  • Ranking and Row Numbering: Functions like RANK(), DENSE_RANK(), and ROW_NUMBER() help in ranking rows, assigning ranks within partitions, and numbering rows, which is useful for ordering and prioritizing data.

  • Partitioning Data: Window functions can partition data into subsets, allowing calculations to be performed within each partition independently, which is essential for segmented analysis and comparison.

  • Enhanced Reporting: By enabling complex calculations directly within SQL queries, window functions streamline the reporting process, reducing the need for additional data processing in application code.

  • Improved Performance: Window functions often improve query performance by reducing the need for subqueries or self-joins, making data retrieval and manipulation more efficient.

  • Versatility: They work seamlessly with various SQL operations and can be combined with other SQL functions, making them versatile tools for a wide range of data manipulation and analysis tasks.

Purpose of the guide

Understanding SQL Window Functions

Definition and basic concept

Comparison with aggregate functions

SQL window functions and aggregate functions both perform calculations across multiple rows, but they do so in fundamentally different ways. Here’s a detailed comparison:

  1. Result Set Preservation:

    • Window Functions: These functions calculate values across a set of rows related to the current row but do not collapse the result set. Each row in the result set retains its individual identity and additional calculated columns are appended to the result.

        sqlCopy codeSELECT 
            employee_id,
            department_id,
            salary,
            AVG(salary) OVER (PARTITION BY department_id) AS avg_department_salary
        FROM employees;
      
    • Aggregate Functions: These functions summarize multiple rows into a single row for each group. They collapse the result set, returning one row per group.

        sqlCopy codeSELECT 
            department_id,
            AVG(salary) AS avg_department_salary
        FROM employees
        GROUP BY department_id;
      
  2. Context and Flexibility:

    • Window Functions: Provide more flexibility as they allow calculations within partitions of data and can be combined with other row-level calculations in the same query. They support a wide range of operations like ranking, running totals, and moving averages within the query context.

        sqlCopy codeSELECT 
            employee_id,
            department_id,
            salary,
            RANK() OVER (PARTITION BY department_id ORDER BY salary DESC) AS salary_rank
        FROM employees;
      
    • Aggregate Functions: Primarily used for summarizing data. They are typically less flexible since they group the data and return a single value for each group.

        sqlCopy codeSELECT 
            department_id,
            COUNT(employee_id) AS num_employees
        FROM employees
        GROUP BY department_id;
      
  3. Partitioning and Ordering:

    • Window Functions: Allow partitioning of data into subsets (using the PARTITION BY clause) and ordering within those subsets (using the ORDER BY clause), enabling complex calculations like row numbering and cumulative sums within each partition.

        sqlCopy codeSELECT 
            order_id,
            order_date,
            SUM(amount) OVER (ORDER BY order_date) AS running_total
        FROM orders;
      
    • Aggregate Functions: Operate on entire groups defined by the GROUP BY clause without considering the order of rows within each group.

        sqlCopy codeSELECT 
            customer_id,
            SUM(amount) AS total_spent
        FROM orders
        GROUP BY customer_id;
      
  4. Use Cases:

    • Window Functions: Ideal for scenarios where detailed row-level data is needed alongside aggregated results, such as running totals, moving averages, ranks, and accessing previous or next row values.

        sqlCopy codeSELECT 
            employee_id,
            salary,
            LAG(salary, 1) OVER (ORDER BY hire_date) AS previous_salary
        FROM employees;
      
    • Aggregate Functions: Best suited for straightforward summarization tasks, like calculating totals, averages, counts, maximum, and minimum values for grouped data.

        sqlCopy codeSELECT 
            department_id,
            MAX(salary) AS highest_salary
        FROM employees
        GROUP BY department_id;
      
  5. Performance Considerations:

    • Window Functions: May be more performance-intensive than aggregate functions due to the complexity of calculations and the need to maintain row context. However, they can eliminate the need for multiple subqueries or joins, potentially improving performance in complex queries.
  • Aggregate Functions: Generally more performant for simple aggregation tasks, as they reduce the data set size and are optimized for grouping and summarizing data.

Syntax of SQL Window Functions

General Structure of SQL Window Functions

The general structure of SQL window functions involves the function itself and the OVER() clause, which defines the window over which the function operates. Here’s a detailed breakdown of the structure:

Basic Syntax

sqlCopy codewindow_function() OVER (window_definition)

Components of the Structure

  1. Window Function:

    • This is the function that performs the calculation. Examples include SUM(), AVG(), ROW_NUMBER(), RANK(), LEAD(), and LAG().
  2. OVER() Clause:

    • The OVER() clause specifies the window definition, which includes PARTITION BY and ORDER BY clauses, and optionally, ROWS or RANGE.
  3. PARTITION BY (Optional):

    • Divides the result set into partitions. The window function is applied to each partition independently.

    • Syntax: PARTITION BY column1, column2, ...

  4. ORDER BY (Optional):

    • Defines the order of rows within each partition. This is crucial for functions like ROW_NUMBER() and cumulative calculations.

    • Syntax: ORDER BY column1 [ASC|DESC], column2 [ASC|DESC], ...

  5. ROWS or RANGE (Optional):

    • Further refines the set of rows within the partition to use for the function.

    • ROWS specifies physical offsets (e.g., number of rows before and after the current row).

    • RANGE specifies logical offsets (e.g., all rows with the same value in the ordering column).

    • Syntax: ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW

Example of basic syntax

  1. Aggregate Function with Partition:

     sqlCopy codeSELECT 
         employee_id,
         department_id,
         salary,
         AVG(salary) OVER (PARTITION BY department_id) AS avg_department_salary
     FROM employees;
    
  2. Aggregate Function with Partition and Order:

     sqlCopy codeSELECT 
         order_date,
         sales_amount,
         SUM(sales_amount) OVER (PARTITION BY customer_id ORDER BY order_date) AS running_total
     FROM sales;
    
  3. Ranking Function:

     sqlCopy codeSELECT 
         employee_id,
         department_id,
         salary,
         RANK() OVER (PARTITION BY department_id ORDER BY salary DESC) AS salary_rank
     FROM employees;
    
  4. Value Function:

     sqlCopy codeSELECT 
         order_id,
         order_date,
         sales_amount,
         LAG(sales_amount, 1) OVER (ORDER BY order_date) AS previous_sales
     FROM sales;
    
  5. Complex Example with ROWS:

     sqlCopy codeSELECT 
         order_date,
         sales_amount,
         AVG(sales_amount) OVER (ORDER BY order_date ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS moving_avg
     FROM sales;
    

Types of SQL Window Functions

Aggregate Window Functions

SUM()

  • Usage: Calculates the sum of a numeric column over a window of rows.

  • Example: Calculate a running total of sales amounts ordered by date.

      sqlCopy codeSELECT 
          order_date,
          sales_amount,
          SUM(sales_amount) OVER (ORDER BY order_date) AS running_total
      FROM sales;
    

AVG()

  • Usage: Computes the average value of a numeric column over a window of rows.

  • Example: Calculate the average salary within each department.

      sqlCopy codeSELECT 
          employee_id,
          department_id,
          salary,
          AVG(salary) OVER (PARTITION BY department_id) AS avg_department_salary
      FROM employees;
    

COUNT()

  • Usage: Counts the number of rows in a window.

  • Example: Count the number of employees in each department.

      sqlCopy codeSELECT 
          employee_id,
          department_id,
          COUNT(*) OVER (PARTITION BY department_id) AS department_count
      FROM employees;
    

MIN() and MAX()

  • Usage: Determines the minimum or maximum value of a column over a window of rows.

  • Example: Find the highest and lowest salary within each department.

      sqlCopy codeSELECT 
          employee_id,
          department_id,
          salary,
          MAX(salary) OVER (PARTITION BY department_id) AS max_salary,
          MIN(salary) OVER (PARTITION BY department_id) AS min_salary
      FROM employees;
    

Ranking Window Functions

ROW_NUMBER()

  • Usage: Assigns a unique sequential integer to rows within a partition, starting at Example: Number employees within each department based on their salary.

      sqlCopy codeSELECT 
          employee_id,
          department_id,
          salary,
          ROW_NUMBER() OVER (PARTITION BY department_id ORDER BY salary DESC) AS row_num
      FROM employees;
    

RANK()

  • Usage: Assigns a rank to each row within a partition, with gaps in ranking if there are ties.

  • Example: Rank employees within each department by salary.

      sqlCopy codeSELECT 
          employee_id,
          department_id,
          salary,
          RANK() OVER (PARTITION BY department_id ORDER BY salary DESC) AS salary_rank
      FROM employees;
    

DENSE_RANK()

  • Usage: Similar to RANK(), but without gaps in ranking when there are ties.

  • Example: Assign dense ranks to employees within each department based on salary.

      sqlCopy codeSELECT 
          employee_id,
          department_id,
          salary,
          DENSE_RANK() OVER (PARTITION BY department_id ORDER BY salary DESC) AS dense_rank
      FROM employees;
    

NTILE()

  • Usage: Distributes rows into a specified number of approximately equal groups.

  • Example: Divide employees into four quartiles within each department based on salary.

      sqlCopy codeSELECT 
          employee_id,
          department_id,
          salary,
          NTILE(4) OVER (PARTITION BY department_id ORDER BY salary DESC) AS quartile
      FROM employees;
    

Value Window Functions

LAG()

  • Usage: Accesses the value from a previous row in the same result set.

  • Example: Get the previous sales amount for each row.

      sqlCopy codeSELECT 
          order_date,
          sales_amount,
          LAG(sales_amount, 1) OVER (ORDER BY order_date) AS previous_sales
      FROM sales;
    

LEAD()

  • Usage: Accesses the value from a subsequent row in the same result set.

  • Example: Get the next sales amount for each row.

      sqlCopy codeSELECT 
          order_date,
          sales_amount,
          LEAD(sales_amount, 1) OVER (ORDER BY order_date) AS next_sales
      FROM sales;
    

FIRST_VALUE()

  • Usage: Returns the first value in an ordered set of values.

  • Example: Get the first sales amount for each partition.

      sqlCopy codeSELECT 
          order_date,
          sales_amount,
          FIRST_VALUE(sales_amount) OVER (PARTITION BY customer_id ORDER BY order_date) AS first_sales
      FROM sales;
    

LAST_VALUE()

  • Usage: Returns the last value in an ordered set of values.

  • Example: Get the last sales amount for each partition.

      sqlCopy codeSELECT 
          order_date,
          sales_amount,
          LAST_VALUE(sales_amount) OVER (PARTITION BY customer_id ORDER BY order_date ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS last_sales
      FROM sales;
    

Cumulative Window Functions

CUME_DIST()

  • Usage: Calculates the cumulative distribution of a value within a partition.

  • Example: Get the cumulative distribution of sales amounts.

      sqlCopy codeSELECT 
          order_date,
          sales_amount,
          CUME_DIST() OVER (ORDER BY sales_amount) AS cume_dist
      FROM sales;
    

PERCENT_RANK()

  • Usage: Calculates the relative rank of a row as a percentage.

  • Example: Get the percentage rank of sales amounts within each partition.

      sqlCopy codeSELECT 
          order_date,
          sales_amount,
          PERCENT_RANK() OVER (PARTITION BY customer_id ORDER BY sales_amount) AS percent_rank
      FROM sales;
    

Practical Examples

Step-by-step walkthroughs with sample data

Here’s a series of step-by-step walkthroughs using SQL window functions with sample data to illustrate their usage:

Sample Data

Let's assume we have the following sample data:

employees Table:

employee_iddepartment_idsalary
110150000
210160000
310170000
410255000
510265000
610275000

sales Table:

order_idorder_datesales_amount
12024-07-011000
22024-07-021500
32024-07-031200
42024-07-041800
52024-07-051300

1. SUM() Window Function

Objective: Calculate the running total of sales amounts.

Query:

sqlCopy codeSELECT 
    order_date,
    sales_amount,
    SUM(sales_amount) OVER (ORDER BY order_date) AS running_total
FROM sales;

Result:

order_datesales_amountrunning_total
2024-07-0110001000
2024-07-0215002500
2024-07-0312003700
2024-07-0418005500
2024-07-0513006800

2. AVG() Window Function

Objective: Calculate the average salary within each department.

Query:

sqlCopy codeSELECT 
    employee_id,
    department_id,
    salary,
    AVG(salary) OVER (PARTITION BY department_id) AS avg_department_salary
FROM employees;

Result:

employee_iddepartment_idsalaryavg_department_salary
11015000060000
21016000060000
31017000060000
41025500065000
51026500065000
61027500065000

3. ROW_NUMBER() Window Function

Objective: Assign a unique sequential integer to employees within each department based on salary.

Query:

sqlCopy codeSELECT 
    employee_id,
    department_id,
    salary,
    ROW_NUMBER() OVER (PARTITION BY department_id ORDER BY salary DESC) AS row_num
FROM employees;

Result:

employee_iddepartment_idsalaryrow_num
3101700001
2101600002
1101500003
6102750001
5102650002
4102550003

4. RANK() Window Function

Objective: Rank employees by salary within each department.

Query:

sqlCopy codeSELECT 
    employee_id,
    department_id,
    salary,
    RANK() OVER (PARTITION BY department_id ORDER BY salary DESC) AS salary_rank
FROM employees;

Result:

employee_iddepartment_idsalarysalary_rank
3101700001
2101600002
1101500003
6102750001
5102650002
4102550003

5. DENSE_RANK() Window Function

Objective: Assign dense ranks to employees within each department based on salary.

Query:

sqlCopy codeSELECT 
    employee_id,
    department_id,
    salary,
    DENSE_RANK() OVER (PARTITION BY department_id ORDER BY salary DESC) AS dense_rank
FROM employees;

Result:

employee_iddepartment_idsalarydense_rank
3101700001
2101600002
1101500003
6102750001
5102650002
4102550003

6. NTILE() Window Function

Objective: Divide employees into four quartiles based on salary.

Query:

sqlCopy codeSELECT 
    employee_id,
    department_id,
    salary,
    NTILE(4) OVER (PARTITION BY department_id ORDER BY salary DESC) AS quartile
FROM employees;

Result:

employee_iddepartment_idsalaryquartile
3101700001
2101600002
1101500003
6102750001
5102650002
4102550003

7. LAG() Window Function

Objective: Retrieve the previous sales amount for each order.

Query:

sqlCopy codeSELECT 
    order_date,
    sales_amount,
    LAG(sales_amount, 1) OVER (ORDER BY order_date) AS previous_sales
FROM sales;

Result:

order_datesales_amountprevious_sales
2024-07-011000NULL
2024-07-0215001000
2024-07-0312001500
2024-07-0418001200
2024-07-0513001800

8. LEAD() Window Function

Objective: Retrieve the next sales amount for each order.

Query:

sqlCopy codeSELECT 
    order_date,
    sales_amount,
    LEAD(sales_amount, 1) OVER (ORDER BY order_date) AS next_sales
FROM sales;

Result:

order_datesales_amountnext_sales
2024-07-0110001500
2024-07-0215001200
2024-07-0312001800
2024-07-0418001300
2024-07-051300NULL

9. FIRST_VALUE() Window Function

Objective: Get the first sales amount for each customer.

Query:

sqlCopy codeSELECT 
    order_date,
    sales_amount,
    FIRST_VALUE(sales_amount) OVER (PARTITION BY order_id ORDER BY order_date) AS first_sales
FROM sales;

Result:

order_datesales_amountfirst_sales
2024-07-0110001000
2024-07-0215001500
2024-07-0312001200
2024-07-0418001800
2024-07-0513001300

10. LAST_VALUE() Window Function

Objective: Get the last sales amount for each customer.

Query:

sqlCopy codeSELECT 
    order_date,
    sales_amount,
    LAST_VALUE(sales_amount) OVER (PARTITION BY order_id ORDER BY order_date ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS last_sales
FROM sales;

Result:

order_datesales_amountlast_sales
2024-07-0110001300
2024-07-0215001300
2024-07-0312001300
2024-07-0418001300
2024-07-0513001300

11. CUME_DIST() Window Function

Objective: Calculate the cumulative distribution of each sales amount.

Query:

sqlCopy codeSELECT 
    order_date,
    sales_amount,
    CUME_DIST() OVER (ORDER BY sales_amount) AS cume_dist
FROM sales;

Result:

order_datesales_amountcume_dist
2024-07-0110000.2
2024-07-0215000.6
2024-07-0312000.4
2024-07-0418001.0
2024-07-0513000.8

12. PERCENT_RANK() Window Function

Objective: Calculate the relative rank of each sales amount as a percentage.

Query:

sqlCopy codeSELECT 
    order_date,
    sales_amount,
    PERCENT_RANK() OVER (ORDER BY sales_amount) AS percent_rank
FROM sales;

Result:

order_datesales_amountpercent_rank
2024-07-0110000.0
2024-07-0215000.5
2024-07-0312000.25
2024-07-0418001.0
2024-07-0513000.75

Advanced Concepts

Frame clauses (ROWS BETWEEN, RANGE BETWEEN)

Frame clauses in SQL window functions define the subset of rows within the partition that the window function should operate on. The two primary types of frame clauses are ROWS and RANGE. Here’s a detailed look at each:

1. ROWS BETWEEN

The ROWS BETWEEN clause specifies a frame of rows relative to the current row. This frame can be a fixed number of rows before and after the current row, or it can be bounded to just the current row.

Syntax:

sqlCopy codeROWS BETWEEN <start> AND <end>

Options for <start> and <end>:

  • UNBOUNDED PRECEDING: Start from the first row in the partition.

  • N PRECEDING: Start from N rows before the current row.

  • CURRENT ROW: Start from the current row.

  • N FOLLOWING: End at N rows after the current row.

  • UNBOUNDED FOLLOWING: End at the last row in the partition.

Examples:

  1. 3 Rows Before and Including the Current Row:

     sqlCopy codeSELECT 
         order_date,
         sales_amount,
         AVG(sales_amount) OVER (ORDER BY order_date ROWS BETWEEN 3 PRECEDING AND CURRENT ROW) AS moving_avg
     FROM sales;
    
    • Calculates a moving average of the current row and the 3 preceding rows.
  2. From the First Row to the Current Row:

     sqlCopy codeSELECT 
         order_date,
         sales_amount,
         SUM(sales_amount) OVER (ORDER BY order_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS cumulative_sum
     FROM sales;
    
    • Computes a cumulative sum from the beginning of the partition to the current row.

2. RANGE BETWEEN

The RANGE BETWEEN clause defines the frame based on the values in a specified column, rather than row numbers. It is typically used for temporal data or numerical data where you want to include rows within a certain range of values.

Syntax:

sqlCopy codeRANGE BETWEEN <start> AND <end>

Options for <start> and <end>:

  • UNBOUNDED PRECEDING: From the start of the partition.

  • N PRECEDING: N units before the current row.

  • CURRENT ROW: The current row.

  • N FOLLOWING: N units after the current row.

  • UNBOUNDED FOLLOWING: To the end of the partition.

Examples:

  1. Range of Values Based on a Column:

     sqlCopy codeSELECT 
         order_date,
         sales_amount,
         AVG(sales_amount) OVER (ORDER BY order_date RANGE BETWEEN INTERVAL '1 MONTH' PRECEDING AND CURRENT ROW) AS moving_avg
     FROM sales;
    
    • Calculates a moving average of sales amounts within the last month relative to the current row.
  2. Fixed Range Based on Numerical Value:

     sqlCopy codeSELECT 
         order_date,
         sales_amount,
         SUM(sales_amount) OVER (ORDER BY sales_amount RANGE BETWEEN 100 PRECEDING AND 100 FOLLOWING) AS total_range
     FROM sales;
    
    • Computes the sum of sales amounts where the sales amount is within 100 units of the current row's sales amount.

Key Differences Between ROWS and RANGE

  • ROWS: Defines the frame based on a physical number of rows relative to the current row. It does not consider the actual values of the rows.

  • RANGE: Defines the frame based on the value of the column being used in the ORDER BY clause. It includes rows where the column values fall within a specified range.

Best Practices for Using Frame Clauses

  1. Choose Appropriately: Use ROWS when you need a fixed number of rows around the current row, and RANGE when your analysis depends on column values or intervals.

  2. Performance Considerations: Be aware of performance impacts, especially with large datasets. Using RANGE with broad intervals or large partitions can be resource-intensive.

  3. Test and Validate: Test your queries with different frame clauses to ensure they produce the correct results and perform efficiently.

By understanding and applying ROWS BETWEEN and RANGE BETWEEN clauses effectively, you can tailor your SQL window functions to fit a wide range of analytical needs and achieve precise, insightful results.

Conclusion

Recap of key points

SQL window functions enable complex calculations over related rows without collapsing them, making tasks like running totals, moving averages, and ranking straightforward. Unlike aggregate functions, window functions retain each row's identity while adding calculated columns. Key components include the OVER() clause with PARTITION BY and ORDER BY options. Types of window functions include aggregate, ranking, value, and cumulative functions. Practical examples illustrate their use, highlighting their non-destructive analysis capability, flexibility, and performance benefits. Understanding and mastering window functions can significantly enhance SQL querying for data analysis and reporting.

0
Subscribe to my newsletter

Read articles from Vishad Patel directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Vishad Patel
Vishad Patel