SQL for Machine Learning Data Access
SQL for Machine Learning Data Access
Learn how to use SQL to retrieve, filter, combine, aggregate, validate, and prepare relational data for Machine Learning projects.
Introduction
Many Machine Learning projects begin with data stored in relational databases. Customer records, transactions, products, invoices, inventory movements, support requests, sensor readings, and operational events are commonly organized into related tables.
SQL, which stands for Structured Query Language, is used to work with relational databases. It allows data professionals to select columns, filter rows, combine tables, calculate summaries, inspect data quality, and create model-ready datasets.
SQL does not replace Python, pandas, or a Machine Learning library. Instead, SQL is commonly used to retrieve and prepare the required information before modeling continues in Python or another analytical environment.
Learning Objectives
By the End of This Lesson
- Explain how SQL supports Machine Learning projects
- Understand databases, tables, rows, columns, keys, and relationships
- Retrieve selected columns using SELECT
- Filter records using WHERE
- Sort and limit query results
- Combine related tables using JOIN
- Create summary features using aggregate functions
- Group observations using GROUP BY
- Use CASE expressions to create derived features
- Work safely with NULL values
- Use common table expressions and window functions
- Create time-aware training labels and features
- Perform data-quality checks using SQL
- Access SQL data securely from Python
- Prevent SQL injection and Machine Learning data leakage
Prerequisites
Recommended Knowledge
- Basic understanding of structured and tabular data
- Basic knowledge of features, targets, and observations
- Basic understanding of the Machine Learning workflow
- Basic familiarity with Python is recommended for the final example
Required Tools
- Access to an authorized relational database
- A database client, query editor, or approved development environment
- Read permission for the required tables or views
- Python, pandas, SQLAlchemy, and a suitable database driver for Python examples
- A Python virtual environment is recommended for local development
Understanding Relational Data
A relational database organizes information into tables. Each table normally represents a business entity, event, or relationship. Tables can be connected using keys.
| Concept | Meaning | Example |
|---|---|---|
| Database | An organized collection of related data | A sales and customer database |
| Table | A structured collection of related rows and columns | Customers |
| Row | One record in a table | One customer |
| Column | One type of information stored for each record | Customer creation date |
| Primary Key | A column or column combination that uniquely identifies a row | CustomerId |
| Foreign Key | A value that connects one table to another table | CustomerId in an Orders table |
| Schema | A namespace that organizes database objects | sales.Customers |
| View | A stored query presented like a table | analytics.CustomerFeatures |
Example Database Schema
The examples in this lesson use a simplified customer-order schema. The exact names and column types in your environment may be different.
| Table | Important Columns | Purpose |
|---|---|---|
Customers |
CustomerId, CreatedDate, Region, Segment, IsActive | Stores customer-level information |
Orders |
OrderId, CustomerId, OrderDate, OrderAmount, OrderStatus | Stores order-level information |
SupportRequests |
RequestId, CustomerId, CreatedDate, Category, Priority | Stores customer-support activity |
CustomerOutcomes |
CustomerId, OutcomeDate, Churned | Stores the target outcome for the example model |
The SELECT Statement
The SELECT statement retrieves data from one or more
database objects.
SELECT
CustomerId,
CreatedDate,
Region,
Segment,
IsActive
FROM Customers;
| SQL Element | Purpose |
|---|---|
SELECT |
Specifies the columns to return |
FROM |
Specifies the source table or view |
CustomerId |
Identifies the customer record |
Region |
Provides a possible categorical feature |
SELECT *
FROM Customers;
SELECT
CustomerId,
CreatedDate,
Region,
Segment
FROM Customers;
Selecting only required columns reduces unnecessary data transfer, makes the feature definition clearer, and lowers the risk of retrieving sensitive or irrelevant information.
Filter Rows with WHERE
The WHERE clause restricts the returned rows according
to one or more conditions.
SELECT
CustomerId,
Region,
Segment
FROM Customers
WHERE IsActive = 1;
Multiple Conditions
SELECT
CustomerId,
Region,
Segment
FROM Customers
WHERE IsActive = 1
AND Region = 'East';
Match Several Values
SELECT
CustomerId,
Region,
Segment
FROM Customers
WHERE Segment IN (
'Standard',
'Premium'
);
Filter a Date Range
SELECT
OrderId,
CustomerId,
OrderDate,
OrderAmount
FROM Orders
WHERE OrderDate >= '2026-01-01'
AND OrderDate < '2026-02-01';
Sort and Limit Results
Use ORDER BY to control result order. During
exploration, a row limit can prevent an unnecessarily large result
from being returned.
SELECT
OrderId,
CustomerId,
OrderDate,
OrderAmount
FROM Orders
ORDER BY OrderDate DESC;
A SQL Server example for returning a sample is:
SELECT TOP (100)
OrderId,
CustomerId,
OrderDate,
OrderAmount
FROM Orders
ORDER BY OrderDate DESC;
Some other database systems use LIMIT instead of
TOP.
Handle NULL Values Correctly
A database NULL normally represents a missing, unknown,
or unavailable value. It is not the same as zero, an empty string,
or the word “NULL.”
Find Missing Values
SELECT
CustomerId,
Region
FROM Customers
WHERE Region IS NULL;
Find Non-Missing Values
SELECT
CustomerId,
Region
FROM Customers
WHERE Region IS NOT NULL;
Replace NULL for Reporting
SELECT
CustomerId,
COALESCE(Region, 'Unknown') AS Region
FROM Customers;
WHERE Region = NULL
WHERE Region IS NULL
Aggregate Functions
Aggregate functions summarize multiple rows into one result. These functions are useful for data profiling and feature engineering.
| Function | Purpose |
|---|---|
COUNT() |
Counts rows or non-NULL values |
SUM() |
Adds numerical values |
AVG() |
Calculates an average |
MIN() |
Returns the minimum value |
MAX() |
Returns the maximum value |
SELECT
COUNT(*) AS OrderCount,
SUM(OrderAmount) AS TotalOrderAmount,
AVG(OrderAmount) AS AverageOrderAmount,
MIN(OrderAmount) AS MinimumOrderAmount,
MAX(OrderAmount) AS MaximumOrderAmount
FROM Orders
WHERE OrderStatus = 'Completed';
COUNT(*) counts rows, while
COUNT(ColumnName) counts non-NULL values in the
specified column. Many other aggregate functions ignore NULL
values.
Group Data with GROUP BY
The GROUP BY clause calculates summaries for different
groups. In Machine Learning feature engineering, it is commonly
used to create one set of aggregate features for each observation,
such as one row per customer.
SELECT
CustomerId,
COUNT(*) AS OrderCount,
SUM(OrderAmount) AS TotalOrderAmount,
AVG(OrderAmount) AS AverageOrderAmount,
MAX(OrderDate) AS MostRecentOrderDate
FROM Orders
WHERE OrderStatus = 'Completed'
GROUP BY CustomerId;
Filter aggregated groups using HAVING:
SELECT
CustomerId,
COUNT(*) AS OrderCount,
SUM(OrderAmount) AS TotalOrderAmount
FROM Orders
WHERE OrderStatus = 'Completed'
GROUP BY CustomerId
HAVING COUNT(*) >= 5;
| Clause | Filters |
|---|---|
WHERE |
Individual rows before grouping |
HAVING |
Aggregated groups after grouping |
Combine Tables with JOIN
Real datasets are frequently distributed across multiple tables. A join combines related rows using a defined relationship.
INNER JOIN
An inner join returns rows for which matching values exist in both joined sources.
SELECT
c.CustomerId,
c.Region,
o.OrderId,
o.OrderDate,
o.OrderAmount
FROM Customers AS c
INNER JOIN Orders AS o
ON o.CustomerId = c.CustomerId;
LEFT JOIN
A left join retains all rows from the left table and returns NULL values when a matching right-side row does not exist.
SELECT
c.CustomerId,
c.Region,
o.OrderId,
o.OrderDate,
o.OrderAmount
FROM Customers AS c
LEFT JOIN Orders AS o
ON o.CustomerId = c.CustomerId;
| Join Type | General Behavior | ML Use Case |
|---|---|---|
| INNER JOIN | Returns only matched rows | Use when unmatched observations should not be retained |
| LEFT JOIN | Retains every row from the left source | Use when every base observation must remain in the dataset |
| FULL OUTER JOIN | Retains matched and unmatched rows from both sides | Useful for reconciliation and data-quality analysis |
Understand Dataset Granularity
Granularity describes what one row represents. Before writing an ML query, define the observation unit clearly.
| Granularity | Meaning of One Row | Possible Model |
|---|---|---|
| Customer Level | One customer | Customer churn prediction |
| Order Level | One order | Late-order prediction |
| Order-Line Level | One product line within an order | Line-level quantity or fulfillment prediction |
| Customer-Month Level | One customer during one month | Monthly behavior prediction |
| Device-Hour Level | One device during one hour | Equipment-condition monitoring |
Detect Row Multiplication After a Join
A join may produce more rows than expected when one key matches multiple records on either side. Compare row and identifier counts before and after a join.
SELECT
COUNT(*) AS TotalRows,
COUNT(DISTINCT CustomerId) AS UniqueCustomers
FROM Customers;
SELECT
COUNT(*) AS JoinedRows,
COUNT(DISTINCT c.CustomerId) AS UniqueCustomers
FROM Customers AS c
LEFT JOIN Orders AS o
ON o.CustomerId = c.CustomerId;
Aggregate Data Before Joining
The following query creates one row per customer from the Orders table before joining the result to Customers.
WITH OrderFeatures AS (
SELECT
CustomerId,
COUNT(*) AS OrderCount,
SUM(OrderAmount) AS TotalOrderAmount,
AVG(OrderAmount) AS AverageOrderAmount,
MAX(OrderDate) AS MostRecentOrderDate
FROM Orders
WHERE OrderStatus = 'Completed'
GROUP BY CustomerId
)
SELECT
c.CustomerId,
c.Region,
c.Segment,
COALESCE(ofe.OrderCount, 0) AS OrderCount,
COALESCE(ofe.TotalOrderAmount, 0) AS TotalOrderAmount,
COALESCE(ofe.AverageOrderAmount, 0) AS AverageOrderAmount,
ofe.MostRecentOrderDate
FROM Customers AS c
LEFT JOIN OrderFeatures AS ofe
ON ofe.CustomerId = c.CustomerId;
Create Features with CASE
A CASE expression creates a derived value based on
conditions.
SELECT
CustomerId,
OrderAmount,
CASE
WHEN OrderAmount >= 1000 THEN 'High'
WHEN OrderAmount >= 500 THEN 'Medium'
ELSE 'Low'
END AS OrderValueBand
FROM Orders;
Create a Binary Indicator
SELECT
CustomerId,
OrderDate,
CASE
WHEN OrderStatus = 'Cancelled' THEN 1
ELSE 0
END AS IsCancelled
FROM Orders;
Build Time-Based Features
Dates and timestamps can be transformed into features representing recency, frequency, duration, weekday, month, season, or time since a previous event.
The following example uses SQL Server-style date functions:
DECLARE @PredictionDate date = '2026-09-01';
SELECT
CustomerId,
DATEDIFF(
day,
CreatedDate,
@PredictionDate
) AS AccountAgeDays
FROM Customers
WHERE CreatedDate <= @PredictionDate;
Count Recent Orders
DECLARE @PredictionDate date = '2026-09-01';
SELECT
CustomerId,
COUNT(*) AS OrdersInPrevious90Days,
SUM(OrderAmount) AS SpendingInPrevious90Days
FROM Orders
WHERE OrderDate < @PredictionDate
AND OrderDate >= DATEADD(
day,
-90,
@PredictionDate
)
AND OrderStatus = 'Completed'
GROUP BY CustomerId;
Prediction Windows and Observation Windows
Time-aware Machine Learning datasets commonly separate the feature observation window from the target prediction window.
| Time Component | Purpose |
|---|---|
| Observation Window | Defines the historical information used to create features |
| Prediction Date | Represents the moment at which the model is assumed to make a prediction |
| Target Window | Defines the future period in which the outcome is measured |
Use Window Functions
Window functions calculate values across related rows without collapsing those rows into one grouped result.
Number Orders Within Each Customer
SELECT
CustomerId,
OrderId,
OrderDate,
ROW_NUMBER() OVER (
PARTITION BY CustomerId
ORDER BY OrderDate DESC
) AS OrderRecencyRank
FROM Orders;
Find the Previous Order Date
SELECT
CustomerId,
OrderId,
OrderDate,
LAG(OrderDate) OVER (
PARTITION BY CustomerId
ORDER BY OrderDate
) AS PreviousOrderDate
FROM Orders;
Create a Running Total
SELECT
CustomerId,
OrderId,
OrderDate,
OrderAmount,
SUM(OrderAmount) OVER (
PARTITION BY CustomerId
ORDER BY OrderDate, OrderId
ROWS BETWEEN UNBOUNDED PRECEDING
AND CURRENT ROW
) AS RunningOrderAmount
FROM Orders;
| Window Function | Typical Purpose |
|---|---|
ROW_NUMBER() |
Assign a sequence number to ordered rows |
RANK() |
Rank rows while allowing shared positions |
LAG() |
Access a previous row's value |
LEAD() |
Access a following row's value |
SUM() OVER |
Calculate cumulative or partition-based totals |
AVG() OVER |
Calculate moving or partition-based averages |
Common Table Expressions
A common table expression, commonly called a CTE, gives a temporary name to a query result. It can make a complex feature query easier to read and maintain.
WITH CompletedOrders AS (
SELECT
OrderId,
CustomerId,
OrderDate,
OrderAmount
FROM Orders
WHERE OrderStatus = 'Completed'
),
CustomerOrderFeatures AS (
SELECT
CustomerId,
COUNT(*) AS OrderCount,
SUM(OrderAmount) AS TotalOrderAmount,
AVG(OrderAmount) AS AverageOrderAmount
FROM CompletedOrders
GROUP BY CustomerId
)
SELECT
c.CustomerId,
c.Region,
c.Segment,
COALESCE(f.OrderCount, 0) AS OrderCount,
COALESCE(f.TotalOrderAmount, 0) AS TotalOrderAmount,
COALESCE(f.AverageOrderAmount, 0) AS AverageOrderAmount
FROM Customers AS c
LEFT JOIN CustomerOrderFeatures AS f
ON f.CustomerId = c.CustomerId;
Create a Supervised Learning Target
In Supervised Learning, the target represents the outcome the model must learn to predict. The target definition should be created consistently and documented clearly.
The following illustrative SQL Server query creates a target indicating whether a customer had a churn outcome within 30 days after the prediction date.
DECLARE @PredictionDate date = '2026-09-01';
SELECT
c.CustomerId,
CASE
WHEN EXISTS (
SELECT 1
FROM CustomerOutcomes AS co
WHERE co.CustomerId = c.CustomerId
AND co.Churned = 1
AND co.OutcomeDate >= @PredictionDate
AND co.OutcomeDate < DATEADD(
day,
30,
@PredictionDate
)
)
THEN 1
ELSE 0
END AS ChurnedWithin30Days
FROM Customers AS c
WHERE c.CreatedDate <= @PredictionDate;
Target-Definition Checklist
- Define the event represented by the target
- Define the prediction date or timestamp
- Define the target observation period
- Define which customers or records are eligible
- Confirm that the target is measured consistently
- Review incomplete outcomes and delayed labels
- Document exclusions and edge cases
Prevent Machine Learning Data Leakage
Data leakage occurs when information that would not legitimately be available at prediction time influences model training or evaluation. SQL feature queries are a common place where leakage can be introduced accidentally.
| Leakage Type | SQL Example | Prevention |
|---|---|---|
| Future Leakage | Aggregating transactions after the prediction date | Apply an explicit upper date boundary |
| Target Leakage | Using an outcome-status column as an input feature | Separate target tables and feature tables |
| Snapshot Leakage | Using the current customer record to represent a historical date | Use historical snapshots or effective-date logic |
| Duplicate Leakage | Returning repeated copies of the same observation | Validate keys and join cardinality |
| Population Leakage | Selecting records using knowledge of the future outcome | Define eligibility using information available at prediction time |
SELECT
CustomerId,
COUNT(*) AS AllKnownOrders
FROM Orders
GROUP BY CustomerId;
If this query is used to create a historical training observation, it may include orders that occurred after that observation's prediction date.
DECLARE @PredictionDate date = '2026-09-01';
SELECT
CustomerId,
COUNT(*) AS HistoricalOrderCount
FROM Orders
WHERE OrderDate < @PredictionDate
GROUP BY CustomerId;
Data-Quality Checks with SQL
SQL can detect missing values, duplicates, invalid ranges, unexpected categories, missing relationships, and inconsistent identifiers before the data is used for Machine Learning.
Count Rows and Unique Keys
SELECT
COUNT(*) AS TotalRows,
COUNT(DISTINCT CustomerId) AS UniqueCustomers
FROM Customers;
Find Duplicate Keys
SELECT
CustomerId,
COUNT(*) AS DuplicateCount
FROM Customers
GROUP BY CustomerId
HAVING COUNT(*) > 1;
Count Missing Values
SELECT
SUM(
CASE
WHEN Region IS NULL THEN 1
ELSE 0
END
) AS MissingRegionCount,
SUM(
CASE
WHEN Segment IS NULL THEN 1
ELSE 0
END
) AS MissingSegmentCount
FROM Customers;
Find Invalid Numerical Values
SELECT
OrderId,
CustomerId,
OrderAmount
FROM Orders
WHERE OrderAmount < 0;
Inspect Category Values
SELECT
Region,
COUNT(*) AS CustomerCount
FROM Customers
GROUP BY Region
ORDER BY CustomerCount DESC;
Find Missing Parent Records
SELECT
o.OrderId,
o.CustomerId
FROM Orders AS o
LEFT JOIN Customers AS c
ON c.CustomerId = o.CustomerId
WHERE c.CustomerId IS NULL;
Review Target Distribution
Classification datasets should be examined for target imbalance. A high overall accuracy can be misleading when one class is much more common than another.
SELECT
Churned,
COUNT(*) AS CustomerCount
FROM CustomerOutcomes
GROUP BY Churned
ORDER BY Churned;
Calculate Class Percentage
SELECT
Churned,
COUNT(*) AS CustomerCount,
100.0 * COUNT(*) /
SUM(COUNT(*)) OVER () AS CustomerPercentage
FROM CustomerOutcomes
GROUP BY Churned
ORDER BY Churned;
Write Efficient Queries
ML extraction queries may process large tables. Efficiency reduces database load, execution time, data transfer, and operational cost.
Query Practices
- Select only required columns
- Filter irrelevant rows as early as practical
- Join using validated keys
- Aggregate transaction data before joining when appropriate
- Avoid unnecessary DISTINCT operations
- Avoid applying functions to indexed filter columns when it prevents efficient access
- Review the execution plan for expensive production queries
- Use approved indexes and managed analytical views
- Test large queries using safe development datasets or limited scopes
- Coordinate resource-intensive extraction with database owners
WHERE YEAR(OrderDate) = 2026
AND MONTH(OrderDate) = 8
WHERE OrderDate >= '2026-08-01'
AND OrderDate < '2026-09-01'
Range-based filtering can make the time boundary clearer and may allow a database to use an appropriate index more effectively. Actual performance still depends on the database, indexes, data distribution, and query plan.
Secure SQL Practices
ML data access must follow the same security principles as any production data application. The model-development process does not justify unrestricted database access.
Security Checklist
- Use a dedicated identity with least-privilege access
- Request read-only access when updates are not required
- Access only approved tables, views, and columns
- Do not place credentials inside source code or notebooks
- Use an approved secret-management system
- Encrypt connections where required
- Protect extracted datasets according to their sensitivity
- Avoid writing confidential data into logs
- Use parameterized queries for external values
- Retain only the data required for the approved purpose
customer_id = user_input
query = (
"SELECT CustomerId, Region "
"FROM Customers "
"WHERE CustomerId = '"
+ customer_id
+ "'"
)
query = """
SELECT
CustomerId,
Region
FROM Customers
WHERE CustomerId = :customer_id
"""
Access SQL Data from Python
Python Prerequisites
- Python installed locally or access to an approved notebook environment
- pandas and SQLAlchemy installed
- A database-specific Python driver installed
- An authorized database account
- Connection details stored outside source code
Install Common Packages
pip install pandas sqlalchemy
Install the database driver required for your selected database. The exact package and connection configuration depend on the database platform and organizational environment.
Parameterized pandas Query
import os
import pandas as pd
from sqlalchemy import create_engine, text
# Read the connection string from a protected environment variable.
connection_string = os.environ[
"ML_DATABASE_CONNECTION_STRING"
]
engine = create_engine(
connection_string
)
query = text("""
SELECT
CustomerId,
Region,
Segment,
CreatedDate
FROM Customers
WHERE IsActive = :is_active
AND CreatedDate < :prediction_date
""")
query_parameters = {
"is_active": 1,
"prediction_date": "2026-09-01"
}
with engine.connect() as connection:
customers = pd.read_sql_query(
query,
connection,
params=query_parameters
)
print(customers.head())
print(customers.shape)
Code Explanation
| Code Element | Purpose |
|---|---|
os.environ |
Reads the connection string without hardcoding it into the script |
create_engine() |
Creates a reusable database connection interface |
text() |
Defines a parameterized SQL statement |
query_parameters |
Stores parameter values separately from SQL text |
engine.connect() |
Opens a managed database connection |
pd.read_sql_query() |
Loads the query result into a pandas DataFrame |
connection_string = (
"database-user-name:"
"real-password@database-server"
)
Load Large Results in Chunks
A query result may be too large to load into memory at once. pandas can return the result in chunks so each portion can be processed separately.
import pandas as pd
from sqlalchemy import text
query = text("""
SELECT
OrderId,
CustomerId,
OrderDate,
OrderAmount
FROM Orders
WHERE OrderDate >= :start_date
AND OrderDate < :end_date
""")
parameters = {
"start_date": "2026-01-01",
"end_date": "2026-02-01"
}
with engine.connect() as connection:
chunks = pd.read_sql_query(
query,
connection,
params=parameters,
chunksize=10000
)
for chunk_number, chunk in enumerate(
chunks,
start=1
):
print(
f"Chunk {chunk_number}: "
f"{len(chunk)} rows"
)
# Perform approved processing here.
End-to-End SQL Feature Dataset
The following SQL Server-style example creates one row per eligible customer. Historical orders and support requests are restricted to dates before the prediction point. The target is measured during a separate future period.
DECLARE @PredictionDate date = '2026-09-01';
DECLARE @ObservationStartDate date = DATEADD(
day,
-90,
@PredictionDate
);
DECLARE @TargetEndDate date = DATEADD(
day,
30,
@PredictionDate
);
WITH EligibleCustomers AS (
SELECT
CustomerId,
CreatedDate,
Region,
Segment
FROM Customers
WHERE CreatedDate <= @PredictionDate
),
OrderFeatures AS (
SELECT
CustomerId,
COUNT(*) AS OrdersInPrevious90Days,
SUM(OrderAmount) AS SpendingInPrevious90Days,
AVG(OrderAmount) AS AverageOrderAmount,
MAX(OrderDate) AS MostRecentOrderDate
FROM Orders
WHERE OrderDate >= @ObservationStartDate
AND OrderDate < @PredictionDate
AND OrderStatus = 'Completed'
GROUP BY CustomerId
),
SupportFeatures AS (
SELECT
CustomerId,
COUNT(*) AS RequestsInPrevious90Days,
SUM(
CASE
WHEN Priority = 'High' THEN 1
ELSE 0
END
) AS HighPriorityRequests
FROM SupportRequests
WHERE CreatedDate >= @ObservationStartDate
AND CreatedDate < @PredictionDate
GROUP BY CustomerId
),
TargetLabels AS (
SELECT
ec.CustomerId,
CASE
WHEN EXISTS (
SELECT 1
FROM CustomerOutcomes AS co
WHERE co.CustomerId = ec.CustomerId
AND co.Churned = 1
AND co.OutcomeDate >= @PredictionDate
AND co.OutcomeDate < @TargetEndDate
)
THEN 1
ELSE 0
END AS ChurnedWithin30Days
FROM EligibleCustomers AS ec
)
SELECT
ec.CustomerId,
ec.Region,
ec.Segment,
DATEDIFF(
day,
ec.CreatedDate,
@PredictionDate
) AS AccountAgeDays,
COALESCE(
ofe.OrdersInPrevious90Days,
0
) AS OrdersInPrevious90Days,
COALESCE(
ofe.SpendingInPrevious90Days,
0
) AS SpendingInPrevious90Days,
COALESCE(
ofe.AverageOrderAmount,
0
) AS AverageOrderAmount,
CASE
WHEN ofe.MostRecentOrderDate IS NULL
THEN NULL
ELSE DATEDIFF(
day,
ofe.MostRecentOrderDate,
@PredictionDate
)
END AS DaysSinceMostRecentOrder,
COALESCE(
sfe.RequestsInPrevious90Days,
0
) AS RequestsInPrevious90Days,
COALESCE(
sfe.HighPriorityRequests,
0
) AS HighPriorityRequests,
tl.ChurnedWithin30Days
FROM EligibleCustomers AS ec
LEFT JOIN OrderFeatures AS ofe
ON ofe.CustomerId = ec.CustomerId
LEFT JOIN SupportFeatures AS sfe
ON sfe.CustomerId = ec.CustomerId
INNER JOIN TargetLabels AS tl
ON tl.CustomerId = ec.CustomerId;
Query Design Explanation
| Query Component | Purpose |
|---|---|
@PredictionDate |
Defines the historical moment at which the prediction is assumed to occur |
@ObservationStartDate |
Defines the beginning of the feature observation window |
@TargetEndDate |
Defines the end of the future target window |
EligibleCustomers |
Defines the model population using information available by the prediction date |
OrderFeatures |
Creates customer-level order features from historical records only |
SupportFeatures |
Creates customer-level support features from historical records only |
TargetLabels |
Measures the future churn outcome separately from the features |
| Final joins | Produce one model-ready row for each eligible customer |
Validate the Final ML Dataset
Before exporting or training from the resulting query, validate its granularity, uniqueness, feature ranges, missing values, and target distribution.
Validate One Row per Customer
WITH FinalDataset AS (
-- Place the final feature query here.
SELECT
CustomerId
FROM Customers
)
SELECT
CustomerId,
COUNT(*) AS RowCount
FROM FinalDataset
GROUP BY CustomerId
HAVING COUNT(*) > 1;
Validate Feature Ranges
WITH FinalDataset AS (
-- Place the final feature query here.
SELECT
CustomerId,
AccountAgeDays,
OrdersInPrevious90Days,
SpendingInPrevious90Days
FROM CustomerFeatureView
)
SELECT
MIN(AccountAgeDays) AS MinimumAccountAgeDays,
MAX(AccountAgeDays) AS MaximumAccountAgeDays,
MIN(OrdersInPrevious90Days) AS MinimumOrderCount,
MAX(OrdersInPrevious90Days) AS MaximumOrderCount,
MIN(SpendingInPrevious90Days) AS MinimumSpending,
MAX(SpendingInPrevious90Days) AS MaximumSpending
FROM FinalDataset;
Validate the Target Distribution
WITH FinalDataset AS (
-- Place the final feature query here.
SELECT
ChurnedWithin30Days
FROM CustomerFeatureView
)
SELECT
ChurnedWithin30Days,
COUNT(*) AS CustomerCount
FROM FinalDataset
GROUP BY ChurnedWithin30Days;
Final Validation Checklist
- One row represents the intended observation unit
- The observation key is unique
- No future information appears in the features
- The target window is separated from the feature window
- Missing values are understood and documented
- Feature ranges are reasonable
- Categorical values use consistent definitions
- Join operations have not multiplied rows unexpectedly
- The target distribution is understood
- The result can be reproduced using the saved SQL and parameters
Reproducibility and Version Control
The SQL used to create a Machine Learning dataset should be treated as project source code. It should be documented, reviewed, tested, and version controlled.
Information to Version
- SQL extraction and feature queries
- Prediction-date parameters
- Feature and target definitions
- Required table and column schema
- Data-quality validation queries
- Expected row granularity
- Dataset-version or snapshot identifiers
- Known exclusions and limitations
Common SQL Mistakes in ML Projects
SQL Data Access Best Practices
Recommended Practices
- Define what one output row represents before writing joins
- Select only required columns
- Use clear table aliases and descriptive feature names
- Apply explicit time boundaries
- Separate historical features from future labels
- Aggregate one-to-many tables before joining when appropriate
- Validate primary keys and join cardinality
- Investigate NULL values before replacing them
- Use parameterized queries for external values
- Use least-privilege database access
- Version SQL with the ML source code
- Test extraction logic using automated data checks
- Monitor schema and source-system changes
- Document feature lineage and business meaning
Hands-On Practice
Retrieve Active Customers
Write a query that returns only the identifier, creation date, region, and segment of active customers.
Create Customer Order Features
Calculate order count, total spending, average order value, and most recent order date for each customer.
Preserve Customers with No Orders
Join the customer table to aggregated order features while retaining customers who have no matching order.
Create a Historical Feature Window
Restrict order features to the 90 days before a defined prediction date.
Create a Future Target
Create a binary target that indicates whether an event occurs during the 30 days after the prediction date.
Validate the Final Dataset
Verify key uniqueness, row count, missing values, feature ranges, and target distribution.
Load the Result into pandas
Use a parameterized query and an approved connection to load the final dataset into a pandas DataFrame.
Knowledge Check
How does SQL support Machine Learning?
Explain how SQL retrieves, combines, aggregates, validates, and prepares relational data for modeling.
What is dataset granularity?
Explain what one row represents and why granularity must be defined before joining tables.
What is the difference between WHERE and HAVING?
Explain which clause filters individual rows and which filters aggregated groups.
Why can a JOIN create duplicate observations?
Explain what happens when one key matches several rows in another table.
Why must feature queries use a prediction timestamp?
Explain how a timestamp separates historical information from future outcomes.
What is SQL injection?
Explain why untrusted values should not be concatenated directly into SQL commands and how parameterized queries help.
Why is one successful query execution not enough?
Explain why row counts, key uniqueness, feature ranges, time boundaries, and target definitions must also be validated.
Why should SQL extraction code be version controlled?
Explain how source control supports reproducibility, review, traceability, and connection to model experiments.
Lesson Summary
| Concept | Main Point |
|---|---|
| SQL | Retrieves and transforms structured relational data |
| SELECT | Specifies the columns returned by a query |
| WHERE | Filters rows before grouping |
| GROUP BY | Creates summaries for defined groups |
| HAVING | Filters aggregated groups |
| JOIN | Combines related information from multiple tables |
| CASE | Creates conditional derived values |
| Window Function | Calculates ordered or partition-based values without collapsing rows |
| CTE | Organizes a query into named logical steps |
| Granularity | Defines what one result row represents |
| Feature Window | Defines the historical period used to create inputs |
| Target Window | Defines the future period used to measure an outcome |
| Data Leakage | Occurs when unavailable or future information influences modeling |
| Parameterized Query | Keeps external values separate from SQL command text |
| Validation | Confirms uniqueness, ranges, missingness, relationships, and target quality |
Key Takeaway
SQL for Machine Learning is not only about retrieving records. A reliable ML query must preserve the intended row granularity, join tables correctly, use time-aware feature boundaries, separate historical features from future labels, validate data quality, prevent leakage, protect sensitive information, and remain reproducible through source control and documented parameters.
Get Machine Learning updates by email
Short, practical lessons and course updates from Rumman Ansari. No spam.