Business analysts live in spreadsheets and dashboards, but the numbers behind both start as a SQL query. This path takes you from confident querying to the exact patterns BAs reach for weekly: KPI rollups, reporting joins, rankings, and period-over-period comparisons.
A Business Analyst's SQL doesn't need to be exotic — it needs to be trustworthy and repeatable. Most of the job is building and defending numbers that go into a report someone's manager will act on: revenue by region, orders by status, top customers this quarter. This path is sequenced around that reality — starting with clean, defensible filtering and grouping, then layering in the joins and ranking logic that real business reporting depends on. Each module has a short explanation, a worked example, and links to go deeper on the homepage lesson player, the blog, or interview questions for this exact topic.
Every report starts with a question about scope: this quarter, this region, this product line.
WHERE is how you answer that precisely, and ORDER BY is how you surface what
actually matters — the top accounts, the biggest gaps, the outliers a stakeholder will ask about
first.
SELECT name, department_id, salary
FROM employees
WHERE department_id = 1
ORDER BY salary DESC;
WHERE clause — sloppy scoping is the #1 reason numbers get questioned in a meeting.Every KPI on a dashboard — total revenue, average order value, headcount by department — is a
GROUP BY underneath. HAVING is what lets you say "only show me departments
over budget," filtering on the rolled-up number rather than the individual rows.
SELECT department_id, COUNT(*) AS headcount, AVG(salary) AS avg_salary
FROM employees
GROUP BY department_id
HAVING AVG(salary) > 70000;
WHERE filters rows before grouping; HAVING filters the finished groups. Confusing the two is the fastest way to get a KPI wrong.Stakeholders think in categories, not raw numbers — "high value," "at risk," "on track." CASE
is how you build those labels directly in a query, so a report can group and count by a business
category without a separate lookup table to maintain.
SELECT name,
CASE
WHEN salary >= 85000 THEN 'Senior band'
WHEN salary >= 65000 THEN 'Mid band'
ELSE 'Entry band'
END AS pay_band
FROM employees;
GROUP BY a CASE expression directly — one query gives you both the label and the rollup by that label.Real reports pull from more than one table — orders need customer names, employees need department
names. INNER JOIN gives you rows with a match on both sides; LEFT JOIN is what
you reach for when the report needs to include everyone, even the customers with zero orders or the
departments with no open projects.
SELECT e.name, d.name AS department, d.location
FROM employees e
LEFT JOIN departments d ON e.department_id = d.id;
INNER JOIN where you needed a LEFT JOIN is the most common culprit."Which employees earn above the company average?" can't be answered by a single flat filter — you
need the average calculated first, then compared against. A subquery in the WHERE clause
is exactly that: a query used as a value inside another query.
SELECT name, salary
FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees);
"Top 5 sales reps by region" and "cumulative revenue through the month" are both window-function
questions. RANK() ranks within a group without collapsing the rows, and SUM() OVER
gives you a running total — both staples of a business report that needs detail and context side by
side.
SELECT department_id, name, salary,
RANK() OVER (PARTITION BY department_id ORDER BY salary DESC) AS dept_rank
FROM employees;
PARTITION BY restarts the ranking or running total per group — it's how "top N per region" becomes a single query instead of one per region.Two questions come up in almost every business review: "what share of the total does this segment
represent?" and "how does this month compare to last?" Both build directly on the window functions from
module 6 — the first divides a row by a window sum, the second compares a row to the previous period
with LAG().
Nothing undermines a business report faster than a number that changes when someone else re-runs your query. NULLs silently dropped in a join, or duplicate rows from an unfiltered join, inflate or deflate totals in ways that are hard to spot until someone challenges the number in a meeting.
COUNT(column) ignores NULLs while COUNT(*) doesn't — decide deliberately which one your KPI needs.These projects use the exact reporting patterns from this path on realistic business datasets.
Business Analyst interviews test exactly what you just practiced: joins across business tables, aggregation logic, and "write me a query that reports X" prompts.
25 questions covering everything above. Score 80% or higher to earn your Business Analyst badge on your dashboard.
25 questions · pass at 80%+ to earn your Business Analyst badge on your dashboard. One-time payment, lifetime access.