1. Lighthouse

Level 2 · Month-over-month vs year-over-year

The request

From Robert Kim, Chief Operating Officer

“Someone told the leadership team that January revenue 'collapsed by half'. Before I react: for every month of 2025, show me growth versus the previous month and versus the same month in 2024.”

Your goal

Using completed orders and lines with quantity > 0, compute monthly net revenue (sum of quantity * unit_price * (1 - discount_pct / 100.0)) for every month in the data. Return the 12 months of 2025 with: month (first day, as a DATE), net_revenue (rounded to 2), mom_growth_pct ((net_revenue - previous month's net_revenue) / previous month's net_revenue * 100, rounded to 1) and yoy_growth_pct ((net_revenue - net_revenue of the same month one year earlier) / that value * 100, rounded to 1). Sort by month ascending.

  • Read from the orders tablenot done yet
  • Read from the order_items tablenot done yet
  • Show exactly month net_revenue mom_growth_pct yoy_growth_pctnot done yet

Your query

SQL

Loading the editor…

    ResultPreview of orders · press Run to see yours
    Preview of orders
    order_idcustomer_idorder_datestatuschannel
    10011252023-01-01 20:51:19completedweb
    10021562023-01-02 07:58:46completedweb
    1003222023-01-02 10:25:27completedmarketplace
    1004482023-01-02 11:21:51completedmarketplace

    Mote checks your result

    Press Run. Each row that matches the goal becomes a firefly in your lantern.

    Tables

    Harbor & Pine store data, ready to query.

    orders12,000 rows · 7 columns
    • order_idinteger
    • customer_idinteger
    • order_datetimestamp
    • statusvarchar
    • channelvarchar
    • shipping_feedecimal
    • coupon_codevarchar
    First 5 rows of orders
    order_idcustomer_idorder_datestatuschannelshipping_feecoupon_code
    10011252023-01-01 20:51:19completedweb0.00NULL
    10021562023-01-02 07:58:46completedweb0.00NULL
    1003222023-01-02 10:25:27completedmarketplace4.99WELCOME10
    1004482023-01-02 11:21:51completedmarketplace4.99WELCOME10
    1005822023-01-02 11:43:36completedweb0.00WELCOME10
    order_items30,338 rows · 6 columns
    • order_item_idinteger
    • order_idinteger
    • product_idinteger
    • quantityinteger
    • unit_pricedecimal
    • discount_pctdecimal
    First 5 rows of order_items
    order_item_idorder_idproduct_idquantityunit_pricediscount_pct
    11001121931.990.00
    21001154311.990.00
    31002154411.990.00
    410021941149.000.00
    51002193219.990.00
    customers2,018 rows · 9 columns
    • customer_idinteger
    • first_namevarchar
    • last_namevarchar
    • emailvarchar
    • cityvarchar
    • statevarchar
    • signup_datedate
    • segmentvarchar
    • referral_sourcevarchar
    First 5 rows of customers
    customer_idfirst_namelast_nameemailcitystatesignup_datesegmentreferral_source
    1OwenHudsonowen.hudson@icloud.comSan FranciscoCA2022-06-02consumersearch
    2DouglasMendozadmendoza69@gmail.comAtlantaGA2022-06-02consumerNULL
    3HenryBrookshbrooks@lakesidecafe.comLos AngelesCA2022-06-04businessNULL
    4BrianGrahambgraham@bluecedarhomes.comSeattleWA2022-06-06businesspinterest
    5KyleJimenezkjimenez27@yahoo.comIndianapolisIN2022-06-07consumerNULL
    products120 rows · 8 columns
    • product_idinteger
    • namevarchar
    • categoryvarchar
    • subcategoryvarchar
    • unit_pricedecimal
    • unit_costdecimal
    • launched_ondate
    • is_activeboolean
    First 5 rows of products
    product_idnamecategorysubcategoryunit_priceunit_costlaunched_onis_active
    101Vale Velvet LoveseatFurnitureSofas1659.00NULL2023-09-11true
    102Alder Oak Coffee TableFurnitureTables919.00503.502024-05-16true
    103Orchard Rattan Dining ChairFurnitureChairs789.00429.872023-05-27true
    104Upland Oak BookcaseFurnitureStorage429.00214.702022-12-20true
    105Avery Leather Bed FrameFurnitureBeds449.00232.702021-08-31true
    returns1,673 rows · 5 columns
    • return_idinteger
    • order_item_idinteger
    • returned_ondate
    • reasonvarchar
    • refund_amountdecimal
    First 5 rows of returns
    return_idorder_item_idreturned_onreasonrefund_amount
    1142023-01-10damaged83.69
    2422023-01-10wrong_item19.99
    31052023-01-14wrong_item10.99
    4242023-01-16changed_mind26.99
    51262023-01-16late20.99
    employees60 rows · 8 columns
    • employee_idinteger
    • first_namevarchar
    • last_namevarchar
    • departmentvarchar
    • titlevarchar
    • manager_idinteger
    • hire_datedate
    • salaryinteger
    First 5 rows of employees
    employee_idfirst_namelast_namedepartmenttitlemanager_idhire_datesalary
    1CatherineHaleExecutiveChief Executive OfficerNULL2016-03-01285000
    2RobertKimExecutiveChief Operating Officer12017-01-09235000
    3MeganCollinsSalesHead of Sales12018-04-16172000
    4ChloeGutierrezSalesAccount Executive32020-01-2172000
    5FrankBellSalesAccount Executive32023-05-2173000
    web_sessions40,000 rows · 7 columns
    • session_idinteger
    • customer_idinteger
    • started_attimestamp
    • devicevarchar
    • sourcevarchar
    • pages_viewedinteger
    • convertedboolean
    First 5 rows of web_sessions
    session_idcustomer_idstarted_atdevicesourcepages_viewedconverted
    1NULL2023-01-01 23:02:24mobilepaid1false
    2182023-01-01 15:07:49desktoppaid1false
    3NULL2023-01-01 16:55:46desktopdirect7false
    4NULL2023-01-01 12:43:28mobileorganic3false
    5NULL2023-01-01 13:06:21mobilesocial2false
    marketing_spend3,276 rows · 3 columns
    • spend_datedate
    • channelvarchar
    • amountdecimal
    First 5 rows of marketing_spend
    spend_datechannelamount
    2023-01-01paid492.99
    2023-01-01email37.38
    2023-01-01social200.53
    2023-01-02paid709.13
    2023-01-02email64.13

    The lesson

    “Are we growing?” has two common answers, and in a seasonal business they often disagree. Month-over-month (MoM) compares a month with the one before it. Year-over-year (YoY) compares it with the same month a year earlier. Good reports show both and know which one to trust.

    LAG with an offset

    LAG(x) looks one row back. LAG(x, n) looks n rows back. On a monthly table with one row per month and no gaps, 12 rows back is the same month last year:

    LAG(net_revenue, 1)  OVER (ORDER BY month) AS prev_month,
    LAG(net_revenue, 12) OVER (ORDER BY month) AS same_month_last_year

    Growth is then (current - previous) / previous * 100. Write 100.0 so the math stays decimal on every database.

    Worked example: the January “collapse”

    Run on completed revenue, three 2025 months tell the story:

    month net_revenue mom_growth_pct yoy_growth_pct
    2025-01-01 150,570.98 -48.2 37.8
    2025-09-01 143,826.50 -21.6 -5.3
    2025-11-01 292,364.40 67.4 -8.4

    January “fell by half” only because December is the holiday peak. Against January 2024 it grew 37.8%. November looks like a triumph on MoM (+67.4%, Black Friday), yet it was 8.4% below November 2024. The YoY column is the one that removes seasonality: it compares like with like.

    The safer YoY: join on the date

    LAG(x, 12) silently breaks when a month is missing, because “12 rows back” is no longer “12 months back”. A self-join on the date never drifts:

    SELECT cur.month, cur.net_revenue, prev.net_revenue AS last_year
    FROM monthly AS cur
    LEFT JOIN monthly AS prev
      ON prev.month = cur.month - INTERVAL 1 YEAR;

    At Harbor & Pine every month has sales, so both forms give the same numbers. On sparse data (a new product, a small region) prefer the join, or build a full calendar first.

    Filter after the window

    Compute LAG over all months and keep 2025 in the outer query. A WHERE on 2025 inside the CTE runs before the window, so January has no December and no month has a previous year.

    Common mistakes

    • Reporting MoM as the headline in a seasonal business. It mostly measures the calendar.
    • Dividing by the wrong base. Growth divides by the earlier value, not the current one.
    • A partial last month. If the current month is not over, both comparisons understate it. The quiz later in this chapter is about exactly that.

    In SQL Server

    LAG(net_revenue, 12) OVER (ORDER BY month) is identical. For the join form, use DATEADD(year, -1, cur.month). Watch integer columns: 100 * (a - b) / b truncates when all values are integers, so keep the 100.0.

    LEFT JOIN monthly AS prev ON prev.month = DATEADD(year, -1, cur.month)

    Pick what may run on this site. You can change this any time from "Cookie settings" at the bottom of every page.