Analyze Ecommerce Orders and Refunds With SQL
Build a merchandise-sales analysis that keeps order activity and refunds separate. Define the measure, choose compatible relationships, and check the totals before presenting the result.
- Release
- v1.0.0
- Tool
- SQLite SQL
- Time
- 30 minutes
What this build produces.
A monthly merchandise-sales table with gross item value, discounts, refunds, and net sales for non-canceled orders.
Know what one row means.
- ordersKey · order_id
- One row per placed order.
- order_itemsKey · order_item_id
- One row per order line.
- refundsKey · refund_id
- One row per successful refund event; an order can have several partial refunds.
- monthly_salesKey · order_month
- One row per UTC order month.
Fix the meaning before the code.
- Gross sales
- Quantity multiplied by order-item unit price for completed, partially refunded, and refunded orders before line discounts and refunds. Canceled orders contribute zero.
- Discounts
- The sum of order_items.discount_amount for included orders, attributed to the order month.
- Refunded amount
- All refund events attached to a completed order, attributed back to the order month even when processed later.
- Net sales
- Gross sales minus discounts and refunds. Tax, shipping, failed payments, and chargebacks are not included.
Work from source grain to tested output.
- 01
Collapse refunds to the order grain
Refund events are more granular than orders. Aggregate first so joining them cannot repeat order revenue.
sql · walkthrough.sqlRefunds by orderProduce at most one refund row for each order.
10 linesWITH refunds_by_order AS ( SELECT order_id, SUM(amount) AS refunded_amount FROM refunds GROUP BY order_id ) SELECT * FROM refunds_by_order ORDER BY order_id;Verification- The result is unique on order_id.
- An order with two partial refunds has one row containing their sum.
- 02
Join at compatible grains and aggregate
Join one refund total to each completed order, then roll the order ledger up to calendar month.
sql · walkthrough.sqlMonthly sales modelCreate the stakeholder-facing monthly sales output.
46 linesWITH items_by_order AS ( SELECT order_id, SUM(quantity * unit_price) AS gross_amount, SUM(discount_amount) AS discount_amount FROM order_items GROUP BY order_id ), refunds_by_order AS ( SELECT order_id, SUM(amount) AS refunded_amount FROM refunds GROUP BY order_id ), order_ledger AS ( SELECT substr(o.ordered_at, 1, 7) AS order_month, o.order_id, i.gross_amount, i.discount_amount, COALESCE(r.refunded_amount, 0) AS refunded_amount FROM orders AS o INNER JOIN items_by_order AS i ON o.order_id = i.order_id LEFT JOIN refunds_by_order AS r ON o.order_id = r.order_id WHERE o.order_status IN ('completed', 'partially_refunded', 'refunded') ), monthly_sales AS ( SELECT order_month, ROUND(SUM(gross_amount), 2) AS gross_sales, ROUND(SUM(discount_amount), 2) AS discounts, ROUND(SUM(refunded_amount), 2) AS refunds, ROUND(SUM(gross_amount - discount_amount - refunded_amount), 2) AS net_sales FROM order_ledger GROUP BY order_month ) SELECT * FROM monthly_sales ORDER BY order_month;Verification- Canceled orders do not contribute sales.
- An order with no refund receives refunded_amount = 0.
- Refund timing does not move revenue between order months.
- 03
Turn the grain and accounting rules into tests
These zero-row checks catch duplicate output keys and arithmetic drift when the release changes.
sqlDuplicate-grain testReturn zero rows when monthly_sales has the promised grain.
6 linesSELECT order_month, COUNT(*) AS row_count FROM monthly_sales GROUP BY order_month HAVING COUNT(*) > 1;sqlNet-sales identity testReturn zero rows when gross minus discounts and refunds equals net sales.
3 linesSELECT * FROM monthly_sales WHERE ABS(gross_sales - discounts - refunds - net_sales) > 0.01;Verification- Both test queries return zero rows.
- The 0.01 tolerance absorbs only the rounding applied to displayed currency amounts.
What this result does not claim.
- Release v1.0.0 is USD-only. Mixed-currency releases must retain currency in the grain or convert before aggregation.
- Refunds without a valid order_id cannot enter the reconciliation and should be quarantined separately.
- Refunds are attributed to the original order month, not the month cash left the business.
- Chargebacks, shipping, tax, and payment processing fees are outside this net-sales definition.
- This item-based merchandise measure differs from the dataset page's checked payment-value example, which includes tax and shipping.
