Why this matters
BI models power decisions. If business logic is unclear or poorly encoded, dashboards contradict each other, audits fail, and teams lose trust. Your job is to translate how the business counts things into consistent, testable data structures and metrics.
- Real tasks you will face: define Active Customer, compute Monthly Recurring Revenue (MRR), calculate churn, implement fiscal calendars, handle refunds and cancellations, enforce row-level security, and align marketing attribution windows.
- Goal: make business definitions explicit and reproducible in the model so every analyst, dashboard, and report agrees.
Concept explained simply
Mapping business logic means turning plain-language rules into unambiguous model definitions.
Mental model: The KPI recipe card
For any metric or dimension, fill this "recipe card":
- Business question: What are we answering?
- Grain: One row represents what? (e.g., one invoice, one subscription-day)
- Who/What to include: Entities, statuses, channels
- Time logic: Event date vs effective date; windows (e.g., last 30 days)
- Counting rule: Distinct count, sum of amounts, average per entity
- Edge cases: Refunds, backfills, test data, partial periods, currency
- Output shape: Fact, dimension, snapshot, or semantic metric
Key patterns you will reuse
- Time intelligence: rolling windows, fiscal calendars, cohort periods, as-of dates
- Status as-of logic: determine status at a date using effective_from/to ranges
- Slowly Changing Dimensions (SCD): keep history of changing attributes
- Many-to-many bridges: orders with multiple campaigns or products
- Allocations: split revenue across months, regions, or product bundles
- Currency conversion: choose rate (transaction-day, month-end) and apply consistently
- Row-level security flags: entitlements mapped to dimension keys
- Late-arriving facts: insert with event timestamps; avoid double counting
Worked examples
Example 1: Active subscribers, MRR, and churn
Scenario: You have subscriptions with start/end dates and invoices. Business definitions:
- Active subscriber on a date: subscription with start_date ≤ date < COALESCE(end_date, +infinity) and not paused
- MRR for a month: sum of monthly_price for subscriptions active on the first day of that month
- Logo churn in a month: count of subscriptions that ended within that month
-- Dimensions/facts (simplified)
-- dim_subscription(id, customer_id, start_date, end_date, status, monthly_price)
-- dim_date(d, first_of_month)
-- Active subscriptions per day
active_subscriptions AS (
SELECT d.d AS as_of_date, s.id AS subscription_id, s.customer_id, s.monthly_price
FROM dim_date d
JOIN dim_subscription s
ON s.start_date <= d.d
AND (s.end_date IS NULL OR d.d < s.end_date)
AND s.status = 'active'
)
-- MRR at month start
mrr_by_month AS (
SELECT dd.first_of_month AS month_start,
SUM(a.monthly_price) AS mrr
FROM active_subscriptions a
JOIN dim_date dd ON dd.d = a.as_of_date
WHERE dd.d = dd.first_of_month
GROUP BY dd.first_of_month
)
-- Logo churn counts
logo_churn AS (
SELECT DATE_TRUNC('month', end_date) AS month_start,
COUNT(DISTINCT id) AS churned_subscriptions
FROM dim_subscription
WHERE end_date IS NOT NULL
GROUP BY 1
)Key mapping choices: "Active" built from date boundaries and status. MRR measured at a consistent reference day. Churn uses end_date month.
Example 2: Marketing first-touch attribution (7-day lookback)
Business rule: Attribute each order to the earliest session within 7 days before the order by the same user.
-- sessions(user_id, session_id, session_ts, channel)
-- orders(user_id, order_id, order_ts, revenue)
WITH candidate AS (
SELECT o.order_id,
o.order_ts,
s.session_id,
s.channel,
s.session_ts,
ROW_NUMBER() OVER (
PARTITION BY o.order_id ORDER BY s.session_ts ASC
) AS rn
FROM orders o
JOIN sessions s
ON s.user_id = o.user_id
AND s.session_ts BETWEEN o.order_ts - INTERVAL '7 days' AND o.order_ts
)
SELECT order_id, order_ts,
session_id AS first_touch_session,
channel AS first_touch_channel
FROM candidate
WHERE rn = 1;Key mapping choices: window definition (7 days), earliest session tie-breaker, and joining by user_id.
Example 3: Inventory on hand as-of date
Business rule: Inventory on hand equals cumulative receipts minus cumulative shipments up to the date.
-- movements(product_id, move_ts, qty_change) -- receipts positive, shipments negative
-- dim_date(d)
SELECT d.d AS as_of_date,
m.product_id,
SUM(m.qty_change) FILTER (WHERE m.move_ts <= d.d) AS qty_on_hand
FROM dim_date d
JOIN movements m ON m.move_ts::date <= d.d
GROUP BY 1,2;Key mapping choices: cumulative sum by as_of_date and product. Ensure not to double-count the same movement across days if you store daily snapshots separately.
Who this is for
- BI Developers and Analytics Engineers defining semantic layers and metrics
- Data Analysts who need consistent, reusable definitions across reports
- Data-savvy PMs validating KPI definitions with data teams
Prerequisites
- Comfort with SQL joins, aggregations, window functions
- Basic data modeling concepts: facts, dimensions, grain
- Understanding of the business domain you are modeling (subscriptions, orders, etc.)
Learning path
- Interview the business: capture KPI recipe cards (grain, filters, time logic, edge cases).
- Choose model shapes: facts, dimensions, snapshots, and bridges.
- Implement time logic: calendars, as-of joins, rolling windows.
- Encode rules: statuses, inclusions/exclusions, currency, allocations.
- Test: unit tests for edge cases; reconcile to known totals.
- Document: plain-language definitions tied to model objects.
Exercises
Complete the tasks below, then check your work. The Quick Test at the end is available to everyone; only logged-in users get saved progress.
Exercise 1: Define Active Customer (last 90 days)
Business rule: A customer is Active if they placed at least one paid order in the last 90 days from an as_of_date.
Tables:
- customers(customer_id, created_at)
- orders(order_id, customer_id, order_ts, status, revenue)
- dim_date(d)
Task: Produce a daily table with as_of_date, customer_id, is_active (0/1).
Exercise 2: Net revenue with refunds
Business rule: Net revenue equals sum of order revenue minus refunds booked within the same month as the order.
Tables:
- orders(order_id, order_ts, revenue, status)
- refunds(refund_id, order_id, refund_ts, refund_amount)
Task: Output month_start, gross_revenue, refunds_applied, net_revenue.
Need a nudge? Open hints
- For Exercise 1, use a semi-join from dim_date to orders with a 90-day window.
- For Exercise 2, align months using DATE_TRUNC on order_ts and refund_ts, but only subtract refunds where refund month equals order month.
Checklist for your model
- [ ] Each metric has a clear grain and time basis (event vs effective)
- [ ] Inclusion/exclusion filters are explicit and testable
- [ ] Edge cases defined: refunds, cancellations, test data, currency
- [ ] One source of truth per metric (no duplicated logic across views)
- [ ] Dimensions are conformed across facts where needed
- [ ] As-of logic covered with date ranges or snapshots
- [ ] Unit/reconciliation tests in place for key KPIs
Common mistakes and how to self-check
- Mixing grains: aggregating daily metrics with monthly prices without aligning to a reference date. Self-check: validate row-grain before joining.
- Inconsistent time: using order_ts for revenue and refund_ts for refunds without alignment. Self-check: decide the month for both and document the choice.
- Implicit filters: silently excluding cancelled orders. Self-check: list all filters next to each metric definition.
- Double counting via many-to-many joins. Self-check: use bridges and distinct counts; test totals against source-of-truth reports.
- Ignoring late-arriving facts. Self-check: compare current-day totals over time to detect backfills.
Practical projects
- Subscription scorecard: active subs, MRR, churn, and expansion MRR with clear monthly reference points.
- Ecommerce funnel: sessions to orders with first-touch attribution and refund-adjusted net revenue.
- Operational SLA: ticket status durations using as-of snapshots and effective dating.
Mini challenge
Write a KPI recipe for "Repeat purchase rate in the last 180 days": define grain, inclusion rules, time window, and edge cases. Then sketch a short SQL snippet that counts customers with 2+ paid orders in the window divided by customers with 1+ paid orders.
Next steps
- Pair with a business stakeholder to validate two KPI recipes; adjust model until a manual sample reconciles.
- Add lightweight documentation inline with your model objects.
- Automate one reconciliation check so it runs daily.
Quick Test
Anyone can take the test. Only logged-in users will have progress saved.