Mathew K Analytics

Lesson 12 · Supply Chain Operations Analytics

Introduction to SQL for Supply Chain Analysts

In this lesson, we will learn how SQL can help analyze real operational data. We will explore how retailers and manufacturers use SQL queries to answer core…

⬇ Download notebookOpen in Colab ↗

📓 Full notebook

Download .ipynb

Introduction to SQL for Supply Chain Analysts#

  • In this lesson, we will learn how SQL can help analyze real operational data.
  • We will explore how retailers and manufacturers use SQL queries to answer core business questions.
  • You will learn to manipulate supply chain datasets, extract insights, and avoid common pitfalls.
  • By the end, you will know how to use SQL-like operations in Python for supply chain analytics.
import pandas as pd
import sqlite3
import warnings
warnings.filterwarnings('ignore')

Understanding Supply Chain Data: What Are We Working With?#

  • Supply chain datasets can represent demand, inventory, supplier performance, or order fulfillment.
  • Operational datasets are usually tables, like SQL tables, with rows and columns.
  • Each row is a transaction or event, such as a sale, shipment, or delivery.
  • Beginners often make mistakes like choosing the wrong keys for joining tables, or ignoring missing data like delivery dates.
  • Understanding your data structure is the foundation for correct SQL analysis.
# Loading a real retail demand dataset as our SQL table
url = 'https://archive.ics.uci.edu/ml/machine-learning-databases/00502/online_retail_II.xlsx'
df = pd.read_excel(url, sheet_name='Year 2010-2011')
df['InvoiceDate'] = pd.to_datetime(df['InvoiceDate'])
print(df.shape)
print(df.head(3))
(541910, 8)
  Invoice StockCode                         Description  Quantity  \
0  536365    85123A  WHITE HANGING HEART T-LIGHT HOLDER         6   
1  536365     71053                 WHITE METAL LANTERN         6   
2  536365    84406B      CREAM CUPID HEARTS COAT HANGER         8   

          InvoiceDate  Price  Customer ID         Country  
0 2010-12-01 08:26:00   2.55      17850.0  United Kingdom  
1 2010-12-01 08:26:00   3.39      17850.0  United Kingdom  
2 2010-12-01 08:26:00   2.75      17850.0  United Kingdom  
# Initialize an in-memory SQLite database and load the table
conn = sqlite3.connect(':memory:')
df.to_sql('retail', conn, index=False, if_exists='replace')
cursor = conn.cursor()

Beginner Example 1: Selecting Columns#

  • Business problem: What are the details of each transaction?
  • We will use SQL SELECT to view specific columns like Invoice, StockCode, and Quantity.
  • This is like viewing selected fields in an Excel spreadsheet.
query = "SELECT Invoice, StockCode, Quantity FROM retail LIMIT 5;"
result = pd.read_sql(query, conn)
print(result)
  Invoice StockCode  Quantity
0  536365    85123A         6
1  536365     71053         6
2  536365    84406B         8
3  536365    84029G         6
4  536365    84029E         6

Beginner Example 2: Filtering Data with WHERE#

  • Business problem: Which transactions happened in France?
  • SQL WHERE clauses let us filter records to answer focused questions.
query = "SELECT Invoice, Country, Quantity FROM retail WHERE Country = 'France' LIMIT 5;"
result = pd.read_sql(query, conn)
print(result)
  Invoice Country  Quantity
0  536370  France        24
1  536370  France        24
2  536370  France        12
3  536370  France        12
4  536370  France        24

Beginner Example 3: Simple Aggregation with GROUP BY#

  • Business problem: How many total items sold per country?
  • SQL GROUP BY lets us roll up numbers for a summary view.
query = "SELECT Country, SUM(Quantity) AS Total_Quantity FROM retail GROUP BY Country ORDER BY Total_Quantity DESC;"
result = pd.read_sql(query, conn)
print(result.head(7))
          Country  Total_Quantity
0  United Kingdom         4263829
1     Netherlands          200128
2            EIRE          142637
3         Germany          117448
4          France          110481
5       Australia           83653
6          Sweden           35637

Intermediate Example 1: Filtering Dates for Sales Trends#

  • Business problem: How many sales invoices did we issue in December 2010?
  • We use SQL WHERE and datetime functions to limit the time range.
query = """
SELECT COUNT(DISTINCT Invoice) AS Dec_Invoices
FROM retail
WHERE strftime('%Y-%m', InvoiceDate) = '2010-12';
"""
result = pd.read_sql(query, conn)
print(result)
   Dec_Invoices
0          2025

Intermediate Example 2: Calculating Revenue per Product#

  • Business problem: Which items earned the most revenue?
  • Operations teams need to find best sellers by joining Quantity and Price in a SQL calculation.
query = """
SELECT StockCode, Description, SUM(Quantity*Price) AS TotalRevenue
FROM retail
GROUP BY StockCode, Description
ORDER BY TotalRevenue DESC
LIMIT 5;
"""
result = pd.read_sql(query, conn)
print(result)
  StockCode                         Description  TotalRevenue
0       DOT                      DOTCOM POSTAGE     206245.48
1     22423            REGENCY CAKESTAND 3 TIER     164762.19
2     47566                       PARTY BUNTING      98302.98
3    85123A  WHITE HANGING HEART T-LIGHT HOLDER      97715.99
4    85099B             JUMBO BAG RED RETROSPOT      92356.03

Intermediate Example 3: Joining Multiple Real Supply Chain Tables#

  • Business problem: Can we analyze orders together with supplier info?
  • SQL JOINs let us merge data from two tables for a richer view.
# Load another real supply chain dataset: supplier performance
import openml
supplier_data = openml.datasets.get_dataset(42125)
df_sup, _, _, _ = supplier_data.get_data(dataset_format='dataframe')
df_sup_small = df_sup[['full_name', 'department', 'division', 'date_first_hired']].drop_duplicates().head(100)
df_sup_small.to_sql('suppliers', conn, index=False, if_exists='replace')
100
query = """
SELECT r.Invoice, r.StockCode, s.department, s.division
FROM retail r
LEFT JOIN suppliers s
ON r.Country = s.division
LIMIT 7;
"""
result = pd.read_sql(query, conn)
print(result)
  Invoice StockCode department division
0  536365    85123A       None     None
1  536365     71053       None     None
2  536365    84406B       None     None
3  536365    84029G       None     None
4  536365    84029E       None     None
5  536365     22752       None     None
6  536365     21730       None     None

Advanced Example 1: Window Functions for Supply Chain KPIs#

  • Business problem: What is the running total revenue by invoice date?
  • Advanced SQL window functions help view trends over timecritical for operations.
query = """
SELECT InvoiceDate,
       SUM(Quantity*Price) AS DailyRevenue,
       SUM(SUM(Quantity*Price)) OVER (ORDER BY InvoiceDate) AS RunningTotal
FROM retail
GROUP BY InvoiceDate
ORDER BY InvoiceDate
LIMIT 7;
"""
try:
    result = pd.read_sql(query, conn)
    print(result)
except Exception as e:
    print('SQLite window functions are only available in some versions. Error:', e)
           InvoiceDate  DailyRevenue  RunningTotal
0  2010-12-01 08:26:00        139.12        139.12
1  2010-12-01 08:28:00         22.20        161.32
2  2010-12-01 08:34:00        348.78        510.10
3  2010-12-01 08:35:00         17.85        527.95
4  2010-12-01 08:45:00        855.86       1383.81
5  2010-12-01 09:00:00        204.00       1587.81
6  2010-12-01 09:01:00         22.20       1610.01

Advanced Example 2: Detecting Anomalies with SQL#

  • Business problem: Find unusually large orders for risk management.
  • Using SQL, we can identify transactions where Quantity is much larger than normal.
query = """
SELECT Invoice, StockCode, Quantity
FROM retail
WHERE Quantity > (
  SELECT AVG(Quantity) + 3* (SELECT AVG(ABS(Quantity-(SELECT AVG(Quantity) FROM retail))) FROM retail)
  FROM retail
)
ORDER BY Quantity DESC
LIMIT 5;
"""
result = pd.read_sql(query, conn)
print(result)
  Invoice StockCode  Quantity
0  581483     23843     80995
1  541431     23166     74215
2  578841     84826     12540
3  542504     37413      5568
4  573008     84077      4800

Advanced Example 3: Subqueries for Complex Metrics#

  • Business problem: Find items only sold in the top 3 countries.
  • Subqueries help us answer niche business questions that require multiple calculations.
query = """
SELECT DISTINCT StockCode, Description
FROM retail
WHERE Country IN (
  SELECT Country FROM retail GROUP BY Country ORDER BY SUM(Quantity) DESC LIMIT 3
)
LIMIT 10;
"""
result = pd.read_sql(query, conn)
print(result)
  StockCode                          Description
0    85123A   WHITE HANGING HEART T-LIGHT HOLDER
1     71053                  WHITE METAL LANTERN
2    84406B       CREAM CUPID HEARTS COAT HANGER
3    84029G  KNITTED UNION FLAG HOT WATER BOTTLE
4    84029E       RED WOOLLY HOTTIE WHITE HEART.
5     22752         SET 7 BABUSHKA NESTING BOXES
6     21730    GLASS STAR FROSTED T-LIGHT HOLDER
7     22633               HAND WARMER UNION JACK
8     22632            HAND WARMER RED POLKA DOT
9     22960             JAM MAKING SET WITH JARS

Error Handling: Missing Data in Operations SQL#

  • Business problem: What happens if an order is missing a delivery date?
  • Missing values are common in real operations, and SQL lets us filter or flag these.
# Find missing delivery dates in another operations dataset
o_url = 'https://raw.githubusercontent.com/olist/work-at-olist-data/master/datasets/olist_orders_dataset.csv'
df_orders = pd.read_csv(o_url)
df_orders['order_purchase_timestamp'] = pd.to_datetime(df_orders['order_purchase_timestamp'])
df_orders['order_delivered_customer_date'] = pd.to_datetime(df_orders['order_delivered_customer_date'])
df_orders.to_sql('orders', conn, index=False, if_exists='replace')
99441
query = "SELECT COUNT(*) AS MissingDeliveries FROM orders WHERE order_delivered_customer_date IS NULL;"
result = pd.read_sql(query, conn)
print(result)
   MissingDeliveries
0               2965

Error Handling: Incorrect SQL Joins#

  • Business problem: What can go wrong if you join tables on the wrong keys?
  • Inconsistent IDs or field names often lead to missing or duplicated data.
# Intentionally join on unrelated fields (bad example)
query = "SELECT r.Invoice, o.order_id FROM retail r INNER JOIN orders o ON r.Invoice = o.order_id LIMIT 5;"
result = pd.read_sql(query, conn)
print(result)
Empty DataFrame
Columns: [Invoice, order_id]
Index: []

Error Handling: Misinterpreting Lead Times or Quantities#

  • Business problem: Negative or missing quantities can break inventory analysis.
  • We should use SQL to filter out or correct such problematic records.
query = "SELECT COUNT(*) as NegativeQuantities FROM retail WHERE Quantity < 0;"
result = pd.read_sql(query, conn)
print(result)
   NegativeQuantities
0               10624

Best Practices: Aggregation and Grouping in SQL#

  • Business problem: Management wants fast summaries, not raw data.
  • Use GROUP BY, SUM, COUNT, and AVG for regular supply chain reporting.
query = "SELECT Country, COUNT(DISTINCT Invoice) AS UniqueInvoices, AVG(Quantity) AS AvgItems FROM retail GROUP BY Country;"
result = pd.read_sql(query, conn)
print(result.head(7))
           Country  UniqueInvoices   AvgItems
0        Australia              69  66.444003
1          Austria              19  12.037406
2          Bahrain               4  13.684211
3          Belgium             119  11.189947
4           Brazil               1  11.125000
5           Canada               6  18.298013
6  Channel Islands              33  12.505277

Best Practices: Calculating Fulfillment Time#

  • Business problem: Operations teams track delivery times as a core metric.
  • Using SQL, we can compute the average time from order to delivery.
query = """
SELECT AVG(julianday(order_delivered_customer_date) - julianday(order_purchase_timestamp)) AS AvgFulfillmentDays
FROM orders
WHERE order_delivered_customer_date IS NOT NULL;
"""
result = pd.read_sql(query, conn)
print(result)
   AvgFulfillmentDays
0           12.558702

Tiny End-to-End Problem: From Raw Data to Insight#

  • Business problem: Which country delivered the highest average revenue per order in 2011?
  • Let us combine filtering, aggregation, and SQL arithmetic for a complete analytics cycle.
query = """
SELECT Country,
       SUM(Quantity*Price)*1.0/COUNT(DISTINCT Invoice) AS AvgOrderRevenue
FROM retail
WHERE strftime('%Y', InvoiceDate) = '2011'
GROUP BY Country
ORDER BY AvgOrderRevenue DESC
LIMIT 5;
"""
result = pd.read_sql(query, conn)
print(result)
       Country  AvgOrderRevenue
0  Netherlands      2815.072041
1    Australia      2093.418000
2      Lebanon      1693.880000
3       Brazil      1143.600000
4        Japan      1105.422000
 

Found this useful?

All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.