1. Lighthouse

Level 6 · Boss: data quality sweep

The request

From Kevin Brooks, Operations Manager

“Finance closes the year soon and I want our data cleaned up first. I need three lists: active products missing a cost, order lines with impossible quantities, and orders from 2025 that are still pending on web or app.”

Your goal

Step 1 of 3. From products, return product_id, name and category for active products (is_active is true) whose unit_cost is NULL.

  • Read from the products tablenot done yet
  • Show exactly product_id name categorynot done yet
  • 8 rowsnot done yet

Step 1 · your query

SQL

Loading the editor…

    ResultPreview of products · press Run to see yours
    Preview of products
    product_idnamecategorysubcategoryunit_price
    101Vale Velvet LoveseatFurnitureSofas1659.00
    102Alder Oak Coffee TableFurnitureTables919.00
    103Orchard Rattan Dining ChairFurnitureChairs789.00
    104Upland Oak BookcaseFurnitureStorage429.00

    Mote checks your result

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

    Step 2 · your query

    SQL

    Loading the editor…

      ResultPreview of order_items · press Run to see yours
      Preview of order_items
      order_item_idorder_idproduct_idquantityunit_price
      11001121931.99
      21001154311.99
      31002154411.99
      410021941149.00

      Mote checks your result

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

      Step 3 · 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.

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

        Data quality checks are a big part of an analyst’s week, especially before a close or a board report. They are also a great way to practice filtering, because each check is just a precise WHERE clause.

        Step 1: active products with no cost

        An active product without a cost breaks every margin report. Two conditions, both required:

        WHERE is_active
          AND unit_cost IS NULL

        Inactive products are excluded on purpose: they are no longer sold, so Kevin does not need them in the clean-up list. Remember from the quiz that unit_cost = NULL would quietly return nothing and make the data look cleaner than it is.

        Step 2: impossible quantities

        A sold line with a quantity of 0 or less is a data error: maybe a return keyed as a negative sale, maybe a test transaction. WHERE quantity <= 0 finds both zero and negative values. In later chapters, when you compute revenue, you will exclude these rows with quantity > 0 and say so in your notes. Analysts who silently include them report revenue that does not match finance.

        Step 3: pending orders in 2025

        Three conditions: status, a pair of channels, and a year. Marketplace orders are handled by the marketplace partner, so Kevin only wants web and app.

        WHERE status = 'pending'
          AND channel IN ('web', 'app')
          AND order_date >= '2025-01-01'
          AND order_date <  '2026-01-01'

        Using IN avoids the AND/OR precedence trap. If you wrote channel = 'web' OR channel = 'app' without parentheses, pending-status and date filters would only apply to the app half, and the list would include every web order ever placed.

        Why a sweep before the close

        Each of these lists feeds a different fix. Missing costs go to merchandising, who own supplier pricing. Impossible quantities go to engineering, because something in checkout or the import job wrote them. Stale pending orders go to operations, who decide whether to ship or cancel. Splitting the results by owner turns one vague “the data is messy” into three tickets that actually get done.

        Reporting back

        When you send results like these, include the count and the rule you used, for example: “8 active products have no cost recorded (is_active = true and unit_cost IS NULL)”. Stating the rule lets the reader catch a misunderstanding before anyone spends a day fixing the wrong rows.