Executive Summary
This project analyzes the Olist Brazilian E-Commerce dataset using MySQL. The full schema contains 8 tables — customers, orders, order_items, order_payments, order_reviews, products, sellers, and geolocation — of which this project's 8 business questions draw on 5: customers, orders, order_items, order_payments, and products. The goal was to turn raw transactional order data into a set of answerable business questions covering revenue trends, customer spend behavior, product category performance, and retention.
My Role
Data Analyst — responsible for designing 8 business questions against the Olist relational schema, writing MySQL queries using multi-table joins, CTEs, and window functions, exporting results for analysis, and translating query output into business findings and recommendations.
Video Walkthrough
Business Context
Order-level, payment-level, and product-level data existed across separate relational tables, but no single view connected customer spend behavior, revenue trends, and product category performance. The goal of this project was to write a focused set of SQL queries that surface who the highest-value customers are, how revenue is trending month to month, which categories and products actually drive revenue, and how much of the customer base returns for a second purchase.
Project Objectives
Write a set of MySQL queries that answer key commercial questions and support data-driven decisions around:
- Top customers by lifetime spend
- Monthly revenue trend and month-over-month change
- Product category and product-level revenue performance
- Customer spend tier segmentation
- Repeat vs. one-time buyer behavior
- Revenue concentration across categories
Business Questions
Each query was written to answer a specific, practical question a commercial or retention-focused analyst would typically ask.
1. Who are the top 10 customers by total amount spent?
Customers, orders, and payments were joined and aggregated to rank customers by lifetime spend.
2. What is the monthly revenue trend across the dataset?
Orders were grouped by purchase month to build a full monthly revenue series from the payments table.
3. What is the month-over-month change in revenue?
A window function (LAG) was used over the monthly revenue series to calculate the change from the previous month.
4. Which product categories generate the most revenue?
Products were joined to order items and grouped by category to rank all 74 categories by total revenue.
5. What are the top 3 products by revenue within each category?
A ranked CTE using ROW_NUMBER() PARTITION BY category surfaces the top 3 revenue-generating products inside every category.
6. How do customers segment into Low / Medium / High spend tiers?
Total spend per customer was calculated and bucketed into tiers using CASE WHEN logic.
7. How many customers are repeat buyers vs. one-time buyers?
ROW_NUMBER() partitioned by customer_unique_id was used to count each customer's total orders and classify them as repeat or one-time.
8. What percent of total revenue comes from the top category?
A SUM() OVER () window function was used to calculate each category's revenue as a percentage of total revenue.
Dataset & Tools Overview
| Category | Details |
|---|---|
| Industry | E-Commerce / Retail |
| Primary Tool | MySQL |
| Dataset | Olist Brazilian E-Commerce (public dataset) |
| Tables Used | customers, orders, order_items, order_payments, products (of 8 total tables) |
| Scope | 99,441 orders, ~90,005 unique customers, 74 product categories |
| Time Span | September 2016 – October 2018 (reliable trend window: Jan 2017 – Aug 2018) |
SQL Queries
All 8 queries below run against the olist schema and use a
combination of multi-table joins, CTEs, and window functions.
Q1. Top 10 Customers by Total Spend
Q1_Top_Customers.sqlJoins customers → orders → payments and ranks customers by lifetime spend.
select
c.customer_id,
round(sum(o2.payment_value), 2) as total_spent,
count(distinct o1.order_id) as num_orders
from customers c
inner join orders o1
on c.customer_id = o1.customer_id
inner join order_payments o2
on o1.order_id = o2.order_id
group by c.customer_id
order by total_spent desc
limit 10;
Q2. Monthly Revenue Trend
Q2_Monthly_Revenue.sqlGroups payments by purchase month to build the full monthly revenue series.
select
date_format(o1.order_purchase_timestamp, '%Y-%m') as month,
format(sum(o2.payment_value), 0) as revenue
from orders o1
inner join order_payments o2
on o1.order_id = o2.order_id
group by month
order by month;
Q3. Month-over-Month Revenue Change
Q3_MoM_Change.sqlUses a CTE plus the LAG() window function to compare each month's revenue against the prior month.
with monthly_revenue as (
select
date_format(order_purchase_timestamp, '%Y-%m') as month,
sum(op.payment_value) as revenue
from orders o
inner join order_payments op
on o.order_id = op.order_id
group by date_format(order_purchase_timestamp, '%Y-%m')
)
select
month,
revenue,
lag(revenue) over (order by month) as previous_month_revenue,
revenue - lag(revenue) over (order by month) as mom_change
from monthly_revenue
order by month;
Q4. Categories by Revenue
Q4_Categories_Highest_Revenue.sqlJoins products to order items and aggregates revenue across all 74 categories.
select
p.product_category_name as product_category,
sum(o.price) as revenue
from products p
inner join order_items o
on p.product_id = o.product_id
group by product_category
order by revenue desc;
Q5. Top 3 Products per Category
Q5_Top3_Products_per_Categories.sqlA ranked CTE using ROW_NUMBER() partitioned by category surfaces each category's top 3 revenue-generating products.
with product_revenue as (
select
p.product_category_name as category,
oi.product_id,
sum(oi.price) as revenue
from products p
inner join order_items oi
on p.product_id = oi.product_id
group by p.product_category_name, oi.product_id
),
ranked_products as (
select
category,
product_id,
revenue,
row_number() over (
partition by category
order by revenue desc
) as product_rank
from product_revenue
)
select category, product_id, revenue, product_rank
from ranked_products
where product_rank <= 3
order by category, product_rank;
Q6. Customer Spend Tiers
Q6_Customer_Segment_Spend_Tiers.sqlSegments every customer into Low / Medium / High spend tiers using CASE WHEN logic.
with customer_spending as (
select
o.customer_id,
sum(op.payment_value) as total_spend
from orders o
inner join order_payments op
on o.order_id = op.order_id
group by o.customer_id
)
select
customer_id,
total_spend,
case
when total_spend < 100 then 'low'
when total_spend < 500 then 'medium'
else 'high'
end as spend_tier
from customer_spending
order by total_spend desc;
Q7. Repeat Buyer vs. One-Time Buyer
Q7_RepeatBuyer_vs_OneTimeBuyer.sqlCounts each customer's total orders via ROW_NUMBER() to classify them as repeat or one-time buyers.
with customer_orders as (
select
o.order_id,
c.customer_unique_id,
row_number() over (
partition by c.customer_unique_id
order by o.order_id
) as purchase_number
from orders o
inner join customers c
on o.customer_id = c.customer_id
),
customer_purchase_counts as (
select
customer_unique_id,
max(purchase_number) as total_purchase
from customer_orders
group by customer_unique_id
)
select
case
when total_purchase = 1 then 'one_time_buyer'
else 'repeat_buyer'
end as customer_type,
count(*) as customer_count
from customer_purchase_counts
group by customer_type;
Q8. Top Category's % of Total Revenue
Q8_Percent_Total_Revenue_Top_Category.sqlUses SUM() OVER () to calculate each category's revenue as a percentage of total order-item revenue.
with category_revenue as (
select
p.product_category_name as category,
sum(oi.price) as revenue
from products p
inner join order_items oi
on p.product_id = oi.product_id
group by p.product_category_name
)
select
category,
revenue,
sum(revenue) over () as total_revenue,
round(revenue / sum(revenue) over () * 100, 2) as percent_of_total
from category_revenue
order by revenue desc;
Key Findings
1. Revenue is concentrated in a handful of categories, led by health & beauty
beleza_saude (health & beauty) is the single largest category, generating R$1,258,681.34 — 9.26% of total order-item revenue (Q8). It isn't an outlier in isolation: the top 5 categories — beleza_saude, relogios_presentes, cama_mesa_banho, esporte_lazer, and informatica_acessorios — together bring in R$5.40M, roughly 40% of all revenue (Q4), out of 74 categories total.
2. The biggest spenders are one-time buyers, not loyal customers — a retention gap
Q1 shows that all 10 of the highest lifetime-spend customers made exactly one order each (the top spender, R$13,664.08, came from a single transaction). That isn't a fluke: Q7 shows 87,347 of 90,005 customers (97.05%) are one-time buyers, versus only 2,658 (2.95%) who are repeat buyers.
3. Revenue growth plateaued through 2018, and the reliable trend line ends in August
Q3's month-over-month view shows revenue climbing from R$138K (Jan 2017) to a peak of R$1.19M in Nov 2017 (a clear holiday/Black Friday spike), before settling into a plateau of roughly R$1.0M–R$1.16M/month from Dec 2017 through Aug 2018 — flat, not growing.
Business Recommendations
1. Launch a Targeted Retention Program for High- and Medium-Tier One-Time Spenders
High PriorityWith 97% of customers never returning (Q7) and the very highest spenders being single-order customers (Q1), a post-purchase win-back campaign — loyalty discount, restock reminder, or bundled offer — aimed at customers in the "high" and "medium" spend tiers from Q6 could convert a meaningful slice of that customer base into repeat buyers, directly reducing dependence on constantly acquiring new customers.
2. Concentrate Category Investment on the Top 5 Revenue Drivers
High PrioritySince beleza_saude, relogios_presentes, cama_mesa_banho, esporte_lazer, and informatica_acessorios already generate ~40% of revenue from just 5 of 74 categories (Q4/Q8), prioritizing ad spend, supplier negotiation, and stock depth for these categories — anchored on the top 3 products per category identified in Q5 — should produce a higher return than distributing budget evenly across the long tail.
Impact / Value
This set of queries transforms raw relational order data into a management-ready view of revenue concentration, trend health, and customer retention risk.
What is happening
- R$13.59M in order-item revenue (R$16.0M on a payments basis) across 99,441 orders
- Top 5 of 74 categories drive ~40% of all revenue
- 97.05% of customers are one-time buyers
- Revenue plateaued at R$1.0M–R$1.16M/month from Dec 2017–Aug 2018
- The final two months of data are almost certainly a truncated extract
Why it matters
- Revenue depends heavily on constant new-customer acquisition, not retention
- A small set of categories carries outsized commercial weight
- Growth had already stalled before the apparent data cutoff
- Trend conclusions must exclude the incomplete Sep–Oct 2018 window
What to do next
- Launch a win-back program for high/medium one-time spenders
- Concentrate category investment on the top 5 revenue drivers
- Use Q5's top-products ranking to select hero SKUs per category
- Re-verify the Sep–Oct 2018 data extract before trusting it
Portfolio Impact Statement
Technical Highlights
SQL techniques used throughout this capstone:
Skills Demonstrated
This project demonstrates the ability to move from Raw Relational Data → SQL Query → Insight → Recommendation → Business Decision.
Project Outcome
This project demonstrates an end-to-end SQL analysis workflow — from writing 8 targeted MySQL queries against a 5-table relational schema, to surfacing revenue concentration, a 97% one-time-buyer retention gap, and a stalled 2018 growth trend, resulting in two concrete, priority-ranked business recommendations.