Skip to content

Sales Analysis

Tables

orders — Orders (status, amount, date)

order_items — Order items (qty, unit price)

products — Products (name, price, stock, brand)

categories — Categories (parent-child hierarchy)

customers — Customers (grade, points, channel)

payments — Payments (method, amount, status)

Concepts

CTE, Window Functions, Multiple JOIN, Aggregation, YoY Growth, Moving Average, ABC Analysis, Cohort

1. Monthly sales trend (last 3 years)

Find monthly sales, number of orders, and average order value from 2022 to 2024.

Hint 1: - Extract year-month with SUBSTR(ordered_at, 1, 7) - SUM(total_amount), COUNT(*), AVG(total_amount)

Answer
SELECT
    SUBSTR(ordered_at, 1, 7)   AS year_month,
    COUNT(*)                   AS order_count,
    ROUND(SUM(total_amount))   AS revenue,
    ROUND(AVG(total_amount))   AS avg_order_value
FROM orders
WHERE status NOT IN ('cancelled', 'returned', 'return_requested')
  AND ordered_at >= '2022-01-01'
  AND ordered_at < '2025-01-01'
GROUP BY SUBSTR(ordered_at, 1, 7)
ORDER BY year_month;

Result (top 7 of 36 rows)

year_month order_count revenue avg_order_value
2022-01 340 387,797,263.00 1,140,580.00
2022-02 343 349,125,148.00 1,017,858.00
2022-03 397 392,750,666.00 989,296.00
2022-04 337 313,546,744.00 930,406.00
2022-05 448 445,361,972.00 994,112.00
2022-06 348 353,057,024.00 1,014,532.00
2022-07 386 418,258,615.00 1,083,572.00

2. Proportion of sales by category

Find the sales of each major category and its proportion (%) compared to the total in 2024.

Hint 1: - categories.depth = 0 is the main category - Subcategory → Middle category → Major category Path: JOIN categories self-reference twice - Or use a subquery to find the top category with depth=0

Answer
WITH category_revenue AS (
    SELECT
        COALESCE(top_cat.name, mid_cat.name, cat.name) AS top_category,
        SUM(oi.quantity * oi.unit_price) AS revenue
    FROM order_items AS oi
    INNER JOIN orders AS o ON oi.order_id = o.id
    INNER JOIN products AS p ON oi.product_id = p.id
    INNER JOIN categories AS cat ON p.category_id = cat.id
    LEFT JOIN categories AS mid_cat ON cat.parent_id = mid_cat.id
    LEFT JOIN categories AS top_cat ON mid_cat.parent_id = top_cat.id
    WHERE o.ordered_at >= '2024-01-01' AND o.ordered_at < '2025-01-01'
      AND o.status NOT IN ('cancelled', 'returned', 'return_requested')
    GROUP BY COALESCE(top_cat.name, mid_cat.name, cat.name)
)
SELECT
    top_category,
    ROUND(revenue) AS revenue,
    ROUND(100.0 * revenue / SUM(revenue) OVER (), 1) AS revenue_pct
FROM category_revenue
ORDER BY revenue DESC;

Result (top 7 of 18 rows)

top_category revenue revenue_pct
Laptop 1,395,635,900.00 27.00
Monitor 727,065,300.00 14.10
Graphics Card 713,579,800.00 13.80
Motherboard 398,988,900.00 7.70
Speakers/Headsets 232,144,800.00 4.50
Storage 205,861,200.00 4.00
Memory (RAM) 200,423,600.00 3.90

3. Top 20 customer sales rankings

Display information about the top 20 customers by total purchase amount for all time periods. Includes customer name, level, number of orders, total purchase amount, and rank.

Hint 1: - Use RANK() or ROW_NUMBER() window functions - customers + orders JOIN

Answer
SELECT
    RANK() OVER (ORDER BY SUM(o.total_amount) DESC) AS ranking,
    c.name          AS customer_name,
    c.grade,
    COUNT(*)        AS order_count,
    ROUND(SUM(o.total_amount)) AS total_spent
FROM customers AS c
INNER JOIN orders AS o ON c.id = o.customer_id
WHERE o.status NOT IN ('cancelled', 'returned', 'return_requested')
GROUP BY c.id, c.name, c.grade
ORDER BY total_spent DESC
LIMIT 20;

Result (top 7 of 20 rows)

ranking customer_name grade order_count total_spent
1 Allen Snyder VIP 303 403,448,758.00
2 Jason Rivera VIP 342 366,385,931.00
3 Brenda Garcia VIP 249 253,180,338.00
4 Courtney Huff VIP 223 244,604,910.00
5 Ronald Arellano VIP 219 235,775,349.00
6 James Banks VIP 230 234,708,853.00
7 Gabriel Walters VIP 275 230,165,991.00

4. Sales pattern by day of the week

Find the average number of orders and average sales by day of the week (Mon-Sun) from all order data. Find out which days of the week have the highest sales.

Hint 1: - SQLite: strftime('%w', ordered_at) → 0 (Sun)~6 (Sat) - Convert day name using CASE statement - First, calculate daily sales and then average them for each day of the week.

Answer
WITH daily_stats AS (
    SELECT
        DATE(ordered_at) AS order_date,
        CAST(strftime('%w', ordered_at) AS INTEGER) AS dow,
        COUNT(*)               AS order_count,
        SUM(total_amount)      AS revenue
    FROM orders
    WHERE status NOT IN ('cancelled', 'returned', 'return_requested')
    GROUP BY DATE(ordered_at)
)
SELECT
    CASE dow
        WHEN 0 THEN '일요일'
        WHEN 1 THEN '월요일'
        WHEN 2 THEN '화요일'
        WHEN 3 THEN '수요일'
        WHEN 4 THEN '목요일'
        WHEN 5 THEN '금요일'
        WHEN 6 THEN '토요일'
    END AS day_of_week,
    ROUND(AVG(order_count)) AS avg_daily_orders,
    ROUND(AVG(revenue))     AS avg_daily_revenue
FROM daily_stats
GROUP BY dow
ORDER BY dow;

Result (7 rows)

day_of_week avg_daily_orders avg_daily_revenue
일요일 11.00 10,702,305.00
월요일 11.00 10,470,017.00
화요일 9.00 9,434,724.00
수요일 9.00 8,818,457.00
목요일 9.00 8,818,498.00
금요일 9.00 9,178,156.00
토요일 11.00 10,550,779.00

5. Quarterly sales and growth rate compared to the previous quarter

Find quarterly sales from 2022 to 2024 and growth rate (%) compared to the previous quarter.

Hint 1: - Branch: (CAST(SUBSTR(ordered_at,6,2) AS INTEGER) + 2) / 3 - Refer to the previous quarter’s sales using the LAG(revenue, 1) window function. - Growth rate = (current quarter - previous quarter) / previous quarter * 100

Answer
WITH quarterly AS (
    SELECT
        SUBSTR(ordered_at, 1, 4) AS year,
        'Q' || ((CAST(SUBSTR(ordered_at, 6, 2) AS INTEGER) + 2) / 3) AS quarter,
        SUBSTR(ordered_at, 1, 4) || '-Q' || ((CAST(SUBSTR(ordered_at, 6, 2) AS INTEGER) + 2) / 3) AS yq,
        ROUND(SUM(total_amount)) AS revenue,
        COUNT(*) AS order_count
    FROM orders
    WHERE status NOT IN ('cancelled', 'returned', 'return_requested')
      AND ordered_at >= '2022-01-01' AND ordered_at < '2025-01-01'
    GROUP BY SUBSTR(ordered_at, 1, 4),
             (CAST(SUBSTR(ordered_at, 6, 2) AS INTEGER) + 2) / 3
)
SELECT
    yq,
    revenue,
    order_count,
    LAG(revenue, 1) OVER (ORDER BY yq) AS prev_quarter_revenue,
    ROUND(100.0 * (revenue - LAG(revenue, 1) OVER (ORDER BY yq))
        / LAG(revenue, 1) OVER (ORDER BY yq), 1) AS qoq_growth_pct
FROM quarterly
ORDER BY yq;

Result (top 7 of 12 rows)

yq revenue order_count prev_quarter_revenue qoq_growth_pct
2022-Q1 1,129,673,077.00 1080 NULL NULL
2022-Q2 1,111,965,740.00 1133 1,129,673,077.00 -1.60
2022-Q3 1,312,284,718.00 1246 1,111,965,740.00 18.00
2022-Q4 1,271,192,508.00 1359 1,312,284,718.00 -3.10
2023-Q1 1,075,250,589.00 1083 1,271,192,508.00 -15.40
2023-Q2 1,026,296,754.00 1102 1,075,250,589.00 -4.60
2023-Q3 1,127,278,823.00 1094 1,026,296,754.00 9.80

6. Trend in sales proportion by payment method

Find the sales share (%) of each payment method (card, bank_transfer, kakao_pay, etc.) by month in 2024.

Hint 1: - Classify payment method as payments.method - Total monthly sales with window function SUM(revenue) OVER (PARTITION BY year_month) - Proportion = Sales by payment method / Total monthly sales * 100

Answer
WITH monthly_method AS (
    SELECT
        SUBSTR(o.ordered_at, 1, 7) AS year_month,
        pm.method,
        ROUND(SUM(pm.amount)) AS revenue
    FROM payments AS pm
    INNER JOIN orders AS o ON pm.order_id = o.id
    WHERE o.ordered_at >= '2024-01-01' AND o.ordered_at < '2025-01-01'
      AND o.status NOT IN ('cancelled', 'returned', 'return_requested')
      AND pm.status = 'paid'
    GROUP BY SUBSTR(o.ordered_at, 1, 7), pm.method
)
SELECT
    year_month,
    method,
    revenue,
    ROUND(100.0 * revenue / SUM(revenue) OVER (PARTITION BY year_month), 1) AS method_pct
FROM monthly_method
ORDER BY year_month, revenue DESC;

7. Top 3 products by category (Top-N per Group)

Select the top three sales products in each major category in 2024.

Hint 1: - Count product sales by category in CTE - ROW_NUMBER() OVER (PARTITION BY category ORDER BY revenue DESC) Ranking - WHERE rn <= 3 filter in outer query

Answer
WITH product_sales AS (
    SELECT
        COALESCE(top_cat.name, mid_cat.name, cat.name) AS top_category,
        p.name AS product_name,
        SUM(oi.quantity)                        AS units_sold,
        ROUND(SUM(oi.quantity * oi.unit_price)) AS revenue
    FROM order_items AS oi
    INNER JOIN orders AS o ON oi.order_id = o.id
    INNER JOIN products AS p ON oi.product_id = p.id
    INNER JOIN categories AS cat ON p.category_id = cat.id
    LEFT JOIN categories AS mid_cat ON cat.parent_id = mid_cat.id
    LEFT JOIN categories AS top_cat ON mid_cat.parent_id = top_cat.id
    WHERE o.ordered_at >= '2024-01-01' AND o.ordered_at < '2025-01-01'
      AND o.status NOT IN ('cancelled', 'returned', 'return_requested')
    GROUP BY COALESCE(top_cat.name, mid_cat.name, cat.name), p.name
),
ranked AS (
    SELECT *,
        ROW_NUMBER() OVER (PARTITION BY top_category ORDER BY revenue DESC) AS rn
    FROM product_sales
)
SELECT top_category, rn AS rank, product_name, units_sold, revenue
FROM ranked
WHERE rn <= 3
ORDER BY top_category, rn;

Result (top 7 of 53 rows)

top_category rank product_name units_sold revenue
CPU 1 AMD Ryzen 9 9900X 239 80,232,300.00
CPU 2 Intel Core Ultra 7 265K White 386 65,697,200.00
Case 1 be quiet! Light Base 900 215 23,054,800.00
Case 2 Fractal Design Define 7 White 108 22,464,000.00
Case 3 CORSAIR iCUE 4000X 196 22,324,400.00
Cooling 1 NZXT Kraken Elite 240 RGB Silver 174 33,876,500.00
Cooling 2 Arctic Liquid Freezer III 240 198 19,522,800.00

8. Year-on-year (YoY) sales growth rate

Find the sales for each month in 2023 and 2024 and the growth rate (%) compared to the same month of the previous year.

Hint 1: - LAG(revenue, 12) — See sales 12 months ago - Or, after separating year + month from CTE, SELF JOIN the previous year of the same month

Answer
WITH monthly AS (
    SELECT
        SUBSTR(ordered_at, 1, 4) AS year,
        SUBSTR(ordered_at, 6, 2) AS month,
        SUBSTR(ordered_at, 1, 7) AS year_month,
        ROUND(SUM(total_amount)) AS revenue
    FROM orders
    WHERE status NOT IN ('cancelled', 'returned', 'return_requested')
      AND ordered_at >= '2022-01-01' AND ordered_at < '2025-01-01'
    GROUP BY SUBSTR(ordered_at, 1, 7)
)
SELECT
    cur.year_month,
    cur.revenue                AS current_revenue,
    prev.revenue               AS prev_year_revenue,
    ROUND(100.0 * (cur.revenue - prev.revenue) / prev.revenue, 1) AS yoy_growth_pct
FROM monthly AS cur
INNER JOIN monthly AS prev
    ON cur.month = prev.month
    AND CAST(cur.year AS INTEGER) = CAST(prev.year AS INTEGER) + 1
WHERE cur.year IN ('2023', '2024')
ORDER BY cur.year_month;

Result (top 7 of 24 rows)

year_month current_revenue prev_year_revenue yoy_growth_pct
2023-01 270,083,587.00 387,797,263.00 -30.40
2023-02 327,431,648.00 349,125,148.00 -6.20
2023-03 477,735,354.00 392,750,666.00 21.60
2023-04 396,849,049.00 313,546,744.00 26.60
2023-05 349,749,072.00 445,361,972.00 -21.50
2023-06 279,698,633.00 353,057,024.00 -20.80
2023-07 312,983,148.00 418,258,615.00 -25.20

9. Moving Average — 3-month moving average of sales

Find the three-month moving average of monthly sales. Moving averages smooth out seasonal fluctuations when identifying trends.

Hint 1: - AVG(revenue) OVER (ORDER BY year_month ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) - There is insufficient data in the first 2 months, so moving averages may not be accurate.

Answer
WITH monthly AS (
    SELECT
        SUBSTR(ordered_at, 1, 7) AS year_month,
        ROUND(SUM(total_amount)) AS revenue
    FROM orders
    WHERE status NOT IN ('cancelled', 'returned', 'return_requested')
      AND ordered_at >= '2023-01-01' AND ordered_at < '2025-01-01'
    GROUP BY SUBSTR(ordered_at, 1, 7)
)
SELECT
    year_month,
    revenue,
    ROUND(AVG(revenue) OVER (
        ORDER BY year_month
        ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
    )) AS moving_avg_3m,
    ROUND(AVG(revenue) OVER (
        ORDER BY year_month
        ROWS BETWEEN 5 PRECEDING AND CURRENT ROW
    )) AS moving_avg_6m
FROM monthly
ORDER BY year_month;

Result (top 7 of 24 rows)

year_month revenue moving_avg_3m moving_avg_6m
2023-01 270,083,587.00 270,083,587.00 270,083,587.00
2023-02 327,431,648.00 298,757,618.00 298,757,618.00
2023-03 477,735,354.00 358,416,863.00 358,416,863.00
2023-04 396,849,049.00 400,672,017.00 368,024,910.00
2023-05 349,749,072.00 408,111,158.00 364,369,742.00
2023-06 279,698,633.00 342,098,918.00 350,257,891.00
2023-07 312,983,148.00 314,143,618.00 357,407,817.00

10. ABC Analysis — Cumulative sales ratio by product

Sort sales by product in descending order in 2024, and assign A/B/C grades based on cumulative sales ratio. (A: Top 70%, B: 70~90%, C: Rest)

Hint 1: - Cumulative ratio: SUM(revenue) OVER (ORDER BY revenue DESC) / SUM(revenue) OVER () - Classification into A/B/C grades using CASE statement - A variation of Pareto's law (80:20)

Answer
WITH product_revenue AS (
    SELECT
        p.id,
        p.name AS product_name,
        ROUND(SUM(oi.quantity * oi.unit_price)) AS revenue
    FROM order_items AS oi
    INNER JOIN orders AS o ON oi.order_id = o.id
    INNER JOIN products AS p ON oi.product_id = p.id
    WHERE o.ordered_at >= '2024-01-01' AND o.ordered_at < '2025-01-01'
      AND o.status NOT IN ('cancelled', 'returned', 'return_requested')
    GROUP BY p.id, p.name
),
cumulative AS (
    SELECT
        product_name,
        revenue,
        SUM(revenue) OVER (ORDER BY revenue DESC) AS cum_revenue,
        SUM(revenue) OVER () AS total_revenue
    FROM product_revenue
)
SELECT
    product_name,
    revenue,
    ROUND(100.0 * cum_revenue / total_revenue, 1) AS cum_pct,
    CASE
        WHEN 100.0 * cum_revenue / total_revenue <= 70 THEN 'A'
        WHEN 100.0 * cum_revenue / total_revenue <= 90 THEN 'B'
        ELSE 'C'
    END AS abc_class
FROM cumulative
ORDER BY revenue DESC
LIMIT 30;

Result (top 7 of 30 rows)

product_name revenue cum_pct abc_class
Razer Blade 18 Black 165,417,800.00 3.20 A
Razer Blade 16 Silver 137,007,300.00 5.90 A
MacBook Air 15 M3 Silver 126,065,300.00 8.30 A
ASUS Dual RTX 4060 Ti Black 106,992,000.00 10.40 A
ASUS Dual RTX 5070 Ti Silver 104,558,400.00 12.40 A
ASUS ROG Swift PG32UCDM Silver 90,734,400.00 14.20 A
ASUS ROG Strix Scar 16 85,837,500.00 15.80 A

11. Comparison of sales from new customers vs. repeat customers

Separate the number of orders and sales from new customers (first order that month) and repeat customers by month in 2024.

Hint 1: - Month of first order for each customer: obtained as MIN(ordered_at) - Order month = “New” if it is the month of first order, otherwise “Repurchase” - Step by step treatment with CTE

Answer
WITH first_order AS (
    SELECT
        customer_id,
        SUBSTR(MIN(ordered_at), 1, 7) AS first_month
    FROM orders
    WHERE status NOT IN ('cancelled', 'returned', 'return_requested')
    GROUP BY customer_id
),
classified AS (
    SELECT
        SUBSTR(o.ordered_at, 1, 7) AS year_month,
        CASE
            WHEN SUBSTR(o.ordered_at, 1, 7) = fo.first_month THEN '신규'
            ELSE '재구매'
        END AS customer_type,
        o.total_amount
    FROM orders AS o
    INNER JOIN first_order AS fo ON o.customer_id = fo.customer_id
    WHERE o.ordered_at >= '2024-01-01' AND o.ordered_at < '2025-01-01'
      AND o.status NOT IN ('cancelled', 'returned', 'return_requested')
)
SELECT
    year_month,
    customer_type,
    COUNT(*)                  AS order_count,
    ROUND(SUM(total_amount)) AS revenue
FROM classified
GROUP BY year_month, customer_type
ORDER BY year_month, customer_type;

Result (top 7 of 24 rows)

year_month customer_type order_count revenue
2024-01 신규 32 31,865,130.00
2024-01 재구매 282 257,043,190.00
2024-02 신규 30 8,770,172.00
2024-02 재구매 386 394,357,577.00
2024-03 신규 50 28,455,371.00
2024-03 재구매 505 491,389,131.00
2024-04 신규 34 25,112,310.00

12. Trend of average unit price by customer level

Find the average order amount by customer level (BRONZE/SILVER/GOLD/VIP) by month in 2024.

Hint 1: - Graded as customers.grade - AVG(total_amount) Aggregation by group - GROUP BY with two dimensions: month + grade

Answer
SELECT
    SUBSTR(o.ordered_at, 1, 7) AS year_month,
    c.grade,
    COUNT(*)                   AS order_count,
    ROUND(AVG(o.total_amount)) AS avg_order_value
FROM orders AS o
INNER JOIN customers AS c ON o.customer_id = c.id
WHERE o.ordered_at >= '2024-01-01' AND o.ordered_at < '2025-01-01'
  AND o.status NOT IN ('cancelled', 'returned', 'return_requested')
GROUP BY SUBSTR(o.ordered_at, 1, 7), c.grade
ORDER BY year_month, 
    CASE c.grade
        WHEN 'VIP' THEN 1
        WHEN 'GOLD' THEN 2
        WHEN 'SILVER' THEN 3
        WHEN 'BRONZE' THEN 4
    END;

Result (top 7 of 48 rows)

year_month grade order_count avg_order_value
2024-01 VIP 124 834,949.00
2024-01 GOLD 73 1,202,930.00
2024-01 SILVER 56 904,820.00
2024-01 BRONZE 61 768,702.00
2024-02 VIP 178 926,721.00
2024-02 GOLD 97 926,946.00
2024-02 SILVER 40 974,420.00

13. Delivery time analysis by shipping company

Find the average delivery days, minimum/maximum lead days, and number of deliveries by carrier in 2024. Only items that have been delivered are eligible.

Hint 1: - Delivery time: JULIANDAY(delivered_at) - JULIANDAY(shipped_at) - status = 'delivered' in table shipping - Only if both shipped_at and delivered_at are NOT NULL.

Answer
SELECT
    s.carrier,
    COUNT(*)                                                        AS delivery_count,
    ROUND(AVG(JULIANDAY(s.delivered_at) - JULIANDAY(s.shipped_at)), 1) AS avg_days,
    MIN(ROUND(JULIANDAY(s.delivered_at) - JULIANDAY(s.shipped_at), 1)) AS min_days,
    MAX(ROUND(JULIANDAY(s.delivered_at) - JULIANDAY(s.shipped_at), 1)) AS max_days,
    ROUND(100.0 * SUM(CASE
        WHEN JULIANDAY(s.delivered_at) - JULIANDAY(s.shipped_at) <= 2 THEN 1
        ELSE 0
    END) / COUNT(*), 1) AS within_2days_pct
FROM shipping AS s
INNER JOIN orders AS o ON s.order_id = o.id
WHERE s.status = 'delivered'
  AND s.shipped_at IS NOT NULL
  AND s.delivered_at IS NOT NULL
  AND o.ordered_at >= '2024-01-01' AND o.ordered_at < '2025-01-01'
GROUP BY s.carrier
ORDER BY avg_days;

Result (5 rows)

carrier delivery_count avg_days min_days max_days within_2days_pct
OnTrac 556 2.40 1.00 4.00 53.80
UPS 1349 2.50 1.00 4.00 52.50
USPS 1068 2.50 1.00 4.00 50.00
DHL 778 2.60 1.00 4.00 46.10
FedEx 1569 2.60 1.00 4.00 47.20

14. Sales impact by discount rate section

Divide the discount rate for orders in 2024 (discount_amount / (total_amount + discount_amount)) into sections, Analyze the number of orders, average order amount, and total sales for each segment.

Hint 1: - Discount rate = discount_amount / (total_amount + discount_amount) * 100 - Categorize into 0%, 1~5%, 6~10%, 11~20%, 20%+ sections using CASE statement - If discount_amount = 0, no discount

Answer
WITH order_discount AS (
    SELECT
        id,
        total_amount,
        discount_amount,
        CASE
            WHEN discount_amount = 0 THEN 0
            ELSE ROUND(100.0 * discount_amount / (total_amount + discount_amount), 1)
        END AS discount_pct
    FROM orders
    WHERE ordered_at >= '2024-01-01' AND ordered_at < '2025-01-01'
      AND status NOT IN ('cancelled', 'returned', 'return_requested')
)
SELECT
    CASE
        WHEN discount_pct = 0    THEN '할인 없음'
        WHEN discount_pct <= 5   THEN '1~5%'
        WHEN discount_pct <= 10  THEN '6~10%'
        WHEN discount_pct <= 20  THEN '11~20%'
        ELSE '20% 초과'
    END AS discount_range,
    COUNT(*)                    AS order_count,
    ROUND(AVG(total_amount))    AS avg_order_value,
    ROUND(SUM(total_amount))    AS total_revenue
FROM order_discount
GROUP BY CASE
    WHEN discount_pct = 0    THEN '할인 없음'
    WHEN discount_pct <= 5   THEN '1~5%'
    WHEN discount_pct <= 10  THEN '6~10%'
    WHEN discount_pct <= 20  THEN '11~20%'
    ELSE '20% 초과'
END
ORDER BY
    CASE
        WHEN discount_pct = 0    THEN 1
        WHEN discount_pct <= 5   THEN 2
        WHEN discount_pct <= 10  THEN 3
        WHEN discount_pct <= 20  THEN 4
        ELSE 5
    END;

Result (4 rows)

discount_range order_count avg_order_value total_revenue
할인 없음 4152 815,975.00 3,387,926,737.00
1~5% 811 1,783,303.00 1,446,258,687.00
6~10% 228 848,082.00 193,362,738.00
11~20% 129 673,607.00 86,895,358.00

15. Promotion ROI analysis

Analyze the sales effect (ROI) of each promotion compared to the input discount amount. During the promotion period, sales and discount amounts for promotional products are counted.

Hint 1: - Identify target product with promotions + promotion_products - Promotion period: started_at ~ ended_at - In order_items, sales for the relevant period + the relevant product are tallied. - ROI = (Sales - Discount Total) / Discount Total * 100

Answer
WITH promo_sales AS (
    SELECT
        pr.id           AS promo_id,
        pr.name         AS promo_name,
        pr.type         AS promo_type,
        pr.discount_type,
        pr.discount_value,
        COUNT(DISTINCT o.id) AS order_count,
        ROUND(SUM(oi.quantity * oi.unit_price)) AS gross_revenue,
        ROUND(SUM(oi.discount_amount))          AS total_discount
    FROM promotions AS pr
    INNER JOIN promotion_products AS pp ON pr.id = pp.promotion_id
    INNER JOIN order_items AS oi ON pp.product_id = oi.product_id
    INNER JOIN orders AS o ON oi.order_id = o.id
    WHERE o.ordered_at >= pr.started_at
      AND o.ordered_at <= pr.ended_at
      AND o.status NOT IN ('cancelled', 'returned', 'return_requested')
    GROUP BY pr.id, pr.name, pr.type, pr.discount_type, pr.discount_value
)
SELECT
    promo_name,
    promo_type,
    discount_type || ' ' || discount_value AS discount_info,
    order_count,
    gross_revenue,
    total_discount,
    gross_revenue - total_discount AS net_revenue,
    CASE
        WHEN total_discount > 0
        THEN ROUND(100.0 * (gross_revenue - total_discount) / total_discount, 1)
        ELSE NULL
    END AS roi_pct
FROM promo_sales
ORDER BY roi_pct DESC;

Result (top 7 of 105 rows)

promo_name promo_type discount_info order_count gross_revenue total_discount net_revenue roi_pct
Gaming Gear Festa 2023 category percent 18.0 25 16,859,400.00 15,600.00 16,843,800.00 107,973.10
Black Friday 2022 seasonal percent 25.0 25 17,059,700.00 18,500.00 17,041,200.00 92,114.60
Gaming Gear Festa 2024 category percent 18.0 30 18,118,000.00 27,600.00 18,090,400.00 65,544.90
Gaming Gear Festa 2020 category percent 18.0 33 12,592,900.00 19,500.00 12,573,400.00 64,479.00
Black Friday 2020 seasonal percent 25.0 35 18,172,900.00 31,300.00 18,141,600.00 57,960.40
Gaming Gear Festa 2016 category percent 18.0 5 5,221,100.00 11,300.00 5,209,800.00 46,104.40
New Year Sale 2021 seasonal percent 10.0 35 33,039,800.00 76,200.00 32,963,600.00 43,259.30

16. Shopping Cart → Purchase Conversion Rate

Find the percentage of products in your shopping cart that were converted into actual purchases by category.

Hint 1: - Number of products contained in cart_items vs. number of identical products actually ordered by the same customer - carts + cart_items + order_items + orders JOIN - Conversion rate = Number of cart items purchased / Total number of cart items * 100

Answer
WITH cart_products AS (
    SELECT
        c.customer_id,
        ci.product_id,
        cat.name AS category
    FROM carts AS c
    INNER JOIN cart_items AS ci ON c.id = ci.cart_id
    INNER JOIN products AS p ON ci.product_id = p.id
    INNER JOIN categories AS cat ON p.category_id = cat.id
),
purchased AS (
    SELECT DISTINCT
        o.customer_id,
        oi.product_id
    FROM orders AS o
    INNER JOIN order_items AS oi ON o.id = oi.order_id
    WHERE o.status NOT IN ('cancelled', 'returned', 'return_requested')
)
SELECT
    cp.category,
    COUNT(*) AS cart_items_total,
    SUM(CASE WHEN pur.product_id IS NOT NULL THEN 1 ELSE 0 END) AS converted,
    ROUND(100.0 * SUM(CASE WHEN pur.product_id IS NOT NULL THEN 1 ELSE 0 END)
        / COUNT(*), 1) AS conversion_rate_pct
FROM cart_products AS cp
LEFT JOIN purchased AS pur
    ON cp.customer_id = pur.customer_id
    AND cp.product_id = pur.product_id
GROUP BY cp.category
ORDER BY conversion_rate_pct DESC;

Result (top 7 of 38 rows)

category cart_items_total converted conversion_rate_pct
Gaming 332 49 14.80
Case 325 37 11.40
SSD 201 22 10.90
DDR4 197 21 10.70
Intel 158 14 8.90
Wireless 205 18 8.80
Speakers/Headsets 398 34 8.50

17. Simultaneous purchase patterns (shopping cart analysis)

Find pairs of products purchased together in the same order. Shows only product pairs with more than 5 simultaneous purchases.

Hint 1: - Self-join order_items to create different product pairs of the same order - Deduplication: oi1.product_id < oi2.product_id - Count the number of simultaneous purchases by GROUP BY product pair

Answer
SELECT
    p1.name AS product_a,
    p2.name AS product_b,
    COUNT(*) AS co_purchase_count
FROM order_items AS oi1
INNER JOIN order_items AS oi2
    ON oi1.order_id = oi2.order_id
    AND oi1.product_id < oi2.product_id
INNER JOIN products AS p1 ON oi1.product_id = p1.id
INNER JOIN products AS p2 ON oi2.product_id = p2.id
INNER JOIN orders AS o ON oi1.order_id = o.id
WHERE o.status NOT IN ('cancelled', 'returned', 'return_requested')
GROUP BY oi1.product_id, oi2.product_id, p1.name, p2.name
HAVING COUNT(*) >= 5
ORDER BY co_purchase_count DESC
LIMIT 20;

Result (top 7 of 20 rows)

product_a product_b co_purchase_count
AMD Ryzen 9 9900X Crucial T700 2TB Silver 430
AMD Ryzen 9 9900X SK hynix Platinum P41 2TB Silver 329
be quiet! Light Base 900 Crucial T700 2TB Silver 294
Intel Core Ultra 5 245KF Crucial T700 2TB Silver 282
be quiet! Light Base 900 AMD Ryzen 9 9900X 249
Seasonic VERTEX GX-1200 Black Crucial T700 2TB Silver 221
Samsung DDR5 32GB PC5-38400 Crucial T700 2TB Silver 217

18. Correlation between review ratings and sales

Analyze the relationship between average review rating and sales for each product. Calculate average sales by rating range (1~2, 2~3, 3~4, 4~5).

Hint 1: - First calculate the average rating and sales for each product - Classification of rating sections using CASE statement - Excluding products without reviews

Answer
WITH product_metrics AS (
    SELECT
        p.id,
        p.name,
        AVG(r.rating)                          AS avg_rating,
        COUNT(DISTINCT r.id)                    AS review_count,
        ROUND(SUM(oi.quantity * oi.unit_price)) AS revenue
    FROM products AS p
    INNER JOIN reviews AS r ON p.id = r.product_id
    INNER JOIN order_items AS oi ON p.id = oi.product_id
    INNER JOIN orders AS o ON oi.order_id = o.id
    WHERE o.status NOT IN ('cancelled', 'returned', 'return_requested')
    GROUP BY p.id, p.name
    HAVING COUNT(DISTINCT r.id) >= 3
)
SELECT
    CASE
        WHEN avg_rating < 2 THEN '1.0~1.9'
        WHEN avg_rating < 3 THEN '2.0~2.9'
        WHEN avg_rating < 4 THEN '3.0~3.9'
        ELSE '4.0~5.0'
    END AS rating_range,
    COUNT(*)                    AS product_count,
    ROUND(AVG(revenue))         AS avg_revenue,
    ROUND(AVG(review_count))    AS avg_reviews,
    ROUND(AVG(avg_rating), 2)   AS avg_rating_in_range
FROM product_metrics
GROUP BY CASE
    WHEN avg_rating < 2 THEN '1.0~1.9'
    WHEN avg_rating < 3 THEN '2.0~2.9'
    WHEN avg_rating < 4 THEN '3.0~3.9'
    ELSE '4.0~5.0'
END
ORDER BY rating_range;

Result (2 rows)

rating_range product_count avg_revenue avg_reviews avg_rating_in_range
3.0~3.9 165 3,987,852,901.00 33.00 3.73
4.0~5.0 99 4,123,862,055.00 30.00 4.15

19. Analysis of point usage effects

Compare the average order amount and repurchase rate for orders that used points and those that did not. (As of 2024)

Hint 1: - If orders.point_used > 0, order using points - Repurchase rate: The percentage of customers in the group who ordered more than twice - First aggregate order characteristics for each customer using CTE

Answer
WITH order_classified AS (
    SELECT
        customer_id,
        total_amount,
        CASE WHEN point_used > 0 THEN '포인트 사용' ELSE '미사용' END AS point_type
    FROM orders
    WHERE ordered_at >= '2024-01-01' AND ordered_at < '2025-01-01'
      AND status NOT IN ('cancelled', 'returned', 'return_requested')
),
customer_stats AS (
    SELECT
        point_type,
        customer_id,
        COUNT(*) AS order_count,
        AVG(total_amount) AS avg_amount
    FROM order_classified
    GROUP BY point_type, customer_id
)
SELECT
    point_type,
    COUNT(DISTINCT customer_id)    AS customer_count,
    ROUND(AVG(avg_amount))         AS avg_order_value,
    ROUND(100.0 * SUM(CASE WHEN order_count >= 2 THEN 1 ELSE 0 END)
        / COUNT(*), 1)            AS repeat_rate_pct
FROM customer_stats
GROUP BY point_type;

Result (2 rows)

point_type customer_count avg_order_value repeat_rate_pct
미사용 1607 895,789.00 57.70
포인트 사용 396 1,033,110.00 18.70

20. Comprehensive management dashboard

Create a comprehensive 2024 management dashboard for CEOs with a single query. Includes total sales, number of orders, number of customers, average unit price, return rate, average delivery date, and average review rating.

Hint 1: - Calculate each indicator individually using subquery or CTE and then combine them - Combine single rows with CROSS JOIN or scalar subqueries - Return rate = Number of returned orders / Total number of orders

Answer
WITH sales AS (
    SELECT
        COUNT(*)                            AS total_orders,
        COUNT(DISTINCT customer_id)         AS unique_customers,
        ROUND(SUM(total_amount))            AS total_revenue,
        ROUND(AVG(total_amount))            AS avg_order_value
    FROM orders
    WHERE ordered_at >= '2024-01-01' AND ordered_at < '2025-01-01'
      AND status NOT IN ('cancelled', 'returned', 'return_requested')
),
returns_stat AS (
    SELECT
        ROUND(100.0 * COUNT(*) / (
            SELECT COUNT(*) FROM orders
            WHERE ordered_at >= '2024-01-01' AND ordered_at < '2025-01-01'
        ), 1) AS return_rate_pct
    FROM orders
    WHERE ordered_at >= '2024-01-01' AND ordered_at < '2025-01-01'
      AND status IN ('returned', 'return_requested')
),
shipping_stat AS (
    SELECT
        ROUND(AVG(JULIANDAY(s.delivered_at) - JULIANDAY(s.shipped_at)), 1) AS avg_delivery_days
    FROM shipping AS s
    INNER JOIN orders AS o ON s.order_id = o.id
    WHERE s.status = 'delivered'
      AND s.shipped_at IS NOT NULL
      AND s.delivered_at IS NOT NULL
      AND o.ordered_at >= '2024-01-01' AND o.ordered_at < '2025-01-01'
),
review_stat AS (
    SELECT
        ROUND(AVG(rating), 2) AS avg_rating,
        COUNT(*) AS review_count
    FROM reviews
    WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01'
)
SELECT
    s.total_revenue,
    s.total_orders,
    s.unique_customers,
    s.avg_order_value,
    r.return_rate_pct,
    sh.avg_delivery_days,
    rv.avg_rating,
    rv.review_count
FROM sales AS s
CROSS JOIN returns_stat AS r
CROSS JOIN shipping_stat AS sh
CROSS JOIN review_stat AS rv;

Result (1 rows)

total_revenue total_orders unique_customers avg_order_value return_rate_pct avg_delivery_days avg_rating review_count
5,114,443,520.00 5320 1669 961,362.00 2.70 2.50 3.91 1267