← Back to Portfolio

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.

R$13.59M
Total Revenue (Order-Item)
99,441
Total Orders
~90,005
Unique Customers
74
Product Categories

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)
Data quality note: November 2016 has zero recorded orders, and September–October 2018 show a >99% month-over-month revenue drop (R$4,439.54 and R$589.67), far more consistent with an incomplete data extract than a real demand collapse. Both periods are excluded from trend conclusions.

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.sql

Joins 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.sql

Groups 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.sql

Uses 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.sql

Joins 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.sql

A 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.sql

Segments 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.sql

Counts 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.sql

Uses 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.

Business takeaway: Inventory, supplier relationships, and marketing spend deliver disproportionate returns when focused on a short list of categories rather than spread evenly.

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.

Business takeaway: Olist's biggest revenue moments — and its customer base as a whole — are driven almost entirely by first-time, high-ticket purchases rather than a returning customer base. There is essentially no retention flywheel currently converting big spenders into repeat relationships.

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 takeaway: This plateau, on its own, is a signal that the business had stopped scaling before the dataset's apparent "collapse" in Sep–Oct 2018, which is almost certainly a truncated extract rather than a real demand crash.

Business Recommendations

1. Launch a Targeted Retention Program for High- and Medium-Tier One-Time Spenders

High Priority

With 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 Priority

Since 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

This project demonstrates how SQL can be used to convert raw, multi-table transactional data into actionable commercial strategy — surfacing a R$13.59M revenue base's category concentration, a 97% one-time-buyer retention gap, and a stalled growth trend, all directly from the relational schema without any external BI tool.

Technical Highlights

SQL techniques used throughout this capstone:

Multi-Table JOINs GROUP BY / HAVING CTEs (WITH clauses) ROW_NUMBER() / PARTITION BY LAG() Window Function SUM() OVER () CASE WHEN Tiering Percent-of-Total Calculations Date Formatting & Grouping Top-N / Ranking Queries

Skills Demonstrated

This project demonstrates the ability to move from Raw Relational Data → SQL Query → Insight → Recommendation → Business Decision.

MySQL SQL Joins Window Functions CTEs Data Aggregation Customer Segmentation Cohort / Retention Analysis Revenue Analysis Business Intelligence Data Quality Assessment Business Insights Data Storytelling

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.