A Business Analyst Workflow From Requirement to Dashboard
Turning a SQL query into a Power BI report fails most often before anyone opens Power BI. The metric is undefined, the query returns the wrong grain, or the audience sees data they should not. This article gives a business analyst workflow from requirement to published dashboard, with the SQL, the DAX, the tests, and the tickets I use.
I have built and tested reports on Epic Clarity, revenue cycle, and finance data. I have also reviewed reports that looked polished and carried wrong numbers. The steps below separate what belongs in SQL, in the model, and in the visuals, so a defect has one obvious home.
A twelve-step workflow. A report specification template. The SQL view, Power Query, and DAX for a working example. Import and DirectQuery guidance. Two scenarios with tickets and before/after code, a UAT checklist, role cards, and a PDF you can print.
SQL Query to Power BI Report: The Workflow in One Table
Think of the path as a chain of handoffs. Each link has an owner, an output, and a check. A report is correct only when every link is correct.
| Step | What happens | Owner | Output | Check |
|---|---|---|---|---|
| 1 | Define the question and the decision | Business analyst | Problem statement | Business owner confirms |
| 2 | Write the metric definitions | Business analyst | Metric sheet | Signed definition |
| 3 | Specify the report | Business analyst | Report spec | Stakeholder review |
| 4 | Profile the source data in SQL | Business analyst or data analyst | Profile queries | Fields populated as expected |
| 5 | Build a governed SQL view | Data analyst or developer | View at a stated grain | Grain and count tests |
| 6 | Connect Power BI and choose a storage mode | Report developer | Connected semantic model | Mode decision recorded |
| 7 | Model the data | Report developer | Star schema, date table, relationships | Relationship tests |
| 8 | Write DAX measures | Report developer | Measures | Match SQL totals |
| 9 | Design the visuals | Report developer and BA | Report pages | Usability review |
| 10 | Secure the data | Report developer and security | Row-level security, workspace roles | View-as tests |
| 11 | Publish, schedule refresh | Report developer | Service report, gateway, refresh | Refresh succeeds |
| 12 | Test, sign off, monitor | QA and business analyst | UAT record | Reconciled and approved |
Notice how many steps happen before the connection in step 6. Analysts who open Power BI first spend the project fixing, in DAX, problems that a better query would have prevented.
RequirementMetric + spec
SQL viewGrain + filters
Semantic modelStar schema
DAX + visualsMeasures, pages
Published reportSecurity + refresh
Owner: BA
Owner: data analyst
Owner: report developer
Owner: security + dev
QA reconciles every stage to the one before it: SQL to source, report to SQL
Defects have one home. A wrong grain is a SQL defect. A wrong ratio is a DAX defect. A wrong audience is a security defect.
Steps 1 to 3: Requirement, Metric Definition, and Report Spec
Power BI is a delivery tool. The business analyst’s main contribution happens in three documents that exist before the first query. They answer who decides what, using which numbers.
Metric sheet
Write one row per metric. The numerator, denominator, exclusions, grain, and owner prevent the disputes that follow a published dashboard. The which tool to master first article and the earlier role comparison use the same template.
| Field | Example |
|---|---|
| Metric | Average length of stay (LOS) |
| Decision supported | Unit managers adjust staffing for discharge flow |
| Definition | Midnights between admit and discharge, averaged over discharges in the period |
| Grain | One row per inpatient discharge |
| Exclusions | Observation stays, newborns, and encounters with no discharge time |
| Owner and version | Director of operations; v1, effective Oct 1 |
Report specification
| Item | What to write |
|---|---|
| Audience and access | Unit managers see their units; directors see all units; no patient-level detail |
| Questions per page | Page 1: How many discharges and what is average LOS? Page 2: Which units changed most? |
| Filters and slicers | Month, unit, service line; default is the last complete month |
| Visuals | Cards for discharges and LOS, trend line by month, ranked bar by unit, table of units |
| Refresh | Daily at 7:00, after the nightly Clarity load completes |
| Performance target | Pages load in under five seconds with default filters |
| Out of scope | Readmissions, financial measures, patient lists |
Get the stakeholder to sign the spec. A signed “out of scope” list does more to protect a schedule than any estimate.
Steps 4 and 5: From SQL Query to Power BI Report Starts With a Governed View
Profile the source before you write the final query. The profile tells you which fields are populated, what values exist, and what the grain is. It also shows data gaps while a fix is still cheap.
-- Profile 1: row count, date range, and nulls in the fields the report needs
SELECT COUNT(*) AS encounters,
MIN(HOSP_DISCH_TIME) AS first_discharge,
MAX(HOSP_DISCH_TIME) AS last_discharge,
SUM(CASE WHEN HOSP_DISCH_TIME IS NULL THEN 1 ELSE 0 END) AS no_discharge_time,
SUM(CASE WHEN DEPARTMENT_ID IS NULL THEN 1 ELSE 0 END) AS no_department
FROM PAT_ENC_HSP
WHERE HOSP_ADMSN_TIME >= '2025-01-01';
-- Profile 2: is the intended key unique?
SELECT PAT_ENC_CSN_ID, COUNT(*) AS copies
FROM PAT_ENC_HSP
GROUP BY PAT_ENC_CSN_ID
HAVING COUNT(*) > 1;
Then build the view. A view gives you one named, reviewable, testable object. Power BI reads it, and so does anyone who needs to check the numbers. The Epic Clarity SQL guide covers the tables behind the example.
CREATE VIEW rpt.vw_discharge AS
SELECT h.PAT_ENC_CSN_ID AS encounter_csn,
CAST(h.HOSP_DISCH_TIME AS date) AS discharge_date,
h.DEPARTMENT_ID AS department_id,
DATEDIFF(day, h.HOSP_ADMSN_TIME, h.HOSP_DISCH_TIME) AS los_days
FROM PAT_ENC_HSP h
WHERE h.HOSP_DISCH_TIME IS NOT NULL
AND h.ADT_PAT_CLASS_C = 1; -- inpatient; confirm the code in your data dictionary
CREATE VIEW rpt.vw_department AS
SELECT d.DEPARTMENT_ID AS department_id, d.DEPARTMENT_NAME AS department_name, m.service_line
FROM CLARITY_DEP d
LEFT JOIN ref.unit_service_line m ON m.department_id = d.DEPARTMENT_ID;
Notice what the view leaves out. No patient identifier, no name, no date of birth. The report asks about volume and length of stay, so minimum necessary says the model needs none of them. If a stakeholder later asks for patient lists, treat it as a new requirement with its own approval.
Tests for the view before Power BI sees it
-- Grain: one row per encounter
SELECT encounter_csn, COUNT(*) FROM rpt.vw_discharge GROUP BY encounter_csn HAVING COUNT(*) > 1; -- expect 0 rows
-- Completeness: view count equals an independent source count
SELECT (SELECT COUNT(*) FROM rpt.vw_discharge WHERE discharge_date >= '2026-09-01' AND discharge_date < '2026-10-01') AS view_rows,
(SELECT COUNT(*) FROM PAT_ENC_HSP WHERE HOSP_DISCH_TIME >= '2026-09-01' AND HOSP_DISCH_TIME < '2026-10-01'
AND ADT_PAT_CLASS_C = 1) AS source_rows;
-- Orphans: every department in the fact exists in the dimension
SELECT DISTINCT f.department_id FROM rpt.vw_discharge f
LEFT JOIN rpt.vw_department d ON d.department_id = f.department_id WHERE d.department_id IS NULL; -- expect 0 rows
These are the same families the ETL validation SQL article uses for warehouse loads. A report view is a small load, and it deserves the same tests.
Step 6: Connect Power BI and Choose Import or DirectQuery
Power BI Desktop connects to SQL Server and many other sources. Connect to the view, not to a pasted query. A pasted query hides logic inside the report file and can stop Power BI from pushing later transformations back to the database.
let
Source = Sql.Database("sqlserver01", "Reporting"),
Discharge = Source{[Schema = "rpt", Item = "vw_discharge"]}[Data]
in
Discharge
Then choose how Power BI stores the data. The decision affects speed, freshness, and which features work. Record it in the spec.
| Factor | Import | DirectQuery |
|---|---|---|
| Where data lives for reports | Copied into the semantic model and refreshed on schedule | Left in the source; each visual sends queries when viewed |
| Freshness | As fresh as the last refresh | Close to live, depending on source load |
| Report speed | Usually fast; data sits in memory | Depends on source and query design |
| Load on the source | At refresh time only | On every interaction |
| Feature coverage | Full Power Query and DAX | Some transformations and functions limited |
| Size limits | Capped by license and capacity; check your tier | Source holds the data |
| Best for | Most departmental dashboards | Very large or near-real-time needs with a tuned source |
Import fits most hospital and finance dashboards because yesterday’s data is enough and users expect speed. Choose DirectQuery only when freshness or size forces it, and tune the source first. The Microsoft Power BI documentation explains composite models, which mix both modes in one model.
Steps 7 and 8: Model the Data and Write DAX Measures
Shape the model as a star: one fact table in the middle, dimension tables around it. Relationships run from each dimension to the fact table, one to many, with a single filter direction.
| Modeling rule | Why |
|---|---|
| One fact table at a stated grain | Every measure counts the same thing |
| Separate dimension tables for department, date, and service line | Filters work the same on every page |
| A complete date table marked as the date table | Time intelligence functions need contiguous dates |
| One-to-many relationships, single direction | Avoids ambiguous filters and double counting |
| Hide keys and technical columns | Report authors see only what is safe to use |
| Measures, not calculated columns, for ratios | Measures respond to the filters in each visual |
Discharges = COUNTROWS ( fact_discharge )
Total LOS Days = SUM ( fact_discharge[los_days] )
Avg LOS = DIVIDE ( [Total LOS Days], [Discharges] )
Discharges PM = CALCULATE ( [Discharges], DATEADD ( dim_date[Date], -1, MONTH ) )
Discharges MoM% = DIVIDE ( [Discharges] - [Discharges PM], [Discharges PM] )
Build ratios from totals, as Avg LOS does. Never average an averaged column. Scenario one below shows how that mistake produces a believable and wrong number. The DAX reference lists each function. The window functions article covers the SQL equivalents of period comparison.
| Logic | Put it in SQL | Put it in Power Query | Put it in DAX |
|---|---|---|---|
| Joins, filters, row-level business rules | Yes | Only if the source cannot | No |
| Column renaming and type fixes | Yes, in the view | Acceptable for report-only needs | No |
| Ratios and totals that respond to filters | No | No | Yes, as measures |
| Time comparisons | For fixed reports | No | Yes, with a date table |
| Sorting and formatting | No | No | Report layer |
The rule I follow is simple. Put row-level logic as low as possible and aggregate logic as high as possible. That keeps one definition per rule and makes defects easy to find.
Steps 9 to 12: Visuals, Security, Refresh, and Sign-Off
Step 9: Design pages around questions
Give each page one question and put the answer first. A card for discharges and average LOS, a trend, and a ranked bar usually answer a unit manager. Put the metric definition in a tooltip or a small info panel, so a viewer who doubts a number can read the rule.
| Design rule | Reason |
|---|---|
| One question per page | Viewers find the answer without searching |
| Default filter set to the last complete month | Partial months look like drops |
| Show the data refresh time on every page | Viewers know how fresh the numbers are |
| Avoid patient identifiers in tooltips and tables | Minimum necessary applies to visuals too |
| Use colors with enough contrast and add labels | Accessibility and printed copies |
| Limit visuals per page | Speed and readability |
Step 10: Secure the data
Row-level security limits which rows a viewer sees. You define roles in Power BI Desktop with DAX filters and assign people to them in the service. A common pattern maps each manager’s sign-in to the departments they own.
-- Role: UnitManagers Table: dim_department Filter expression:
[manager_email] = USERPRINCIPALNAME ()
Filters flow from dimension to fact through relationships, so this one filter also restricts the discharge rows. Test each role with the “View as” feature before publishing. Then test it again in the service.
Row-level security applies to viewers. People with edit permission in a workspace, such as members and contributors, can see all the data. Give report consumers the viewer role or share through an app, and keep editor roles for the build team. Confirm current behavior in the Microsoft documentation for your tenant.
Step 11: Publish and schedule refresh
Publish from Desktop to a workspace. If the SQL server sits inside your network, the service reaches it through an on-premises data gateway. Use a service account for credentials, not a personal sign-in that expires when someone changes jobs.
| Refresh decision | Guidance |
|---|---|
| When to refresh | After the source load finishes. A Clarity nightly load may end at an unpredictable hour, so check the job history before you set a time |
| Who gets alerts | Name an owner and a backup; send failures to a monitored address |
| Credentials | Service account with read-only rights to the reporting schema |
| Incremental data | Consider incremental refresh for large fact tables; confirm license support |
| After a source change | Retest the view tests and refresh the model before users see the change |
Step 12: Test, reconcile, and sign off
| Test | Method | Pass rule |
|---|---|---|
| Totals tie to SQL | Compare card values to a direct query for the same filters | Exact match |
| Ratios tie to SQL | Compute SUM over COUNT in SQL for two units and one month | Matches to the displayed decimals |
| Filters behave | Apply unit, month, and service line filters in combinations | Totals respond as expected |
| Blank and edge periods | Select a month with no discharges and the current partial month | Blank or labeled, no errors |
| Security | “View as” each role; test one user outside all roles | Sees only permitted rows; no-role user sees nothing |
| Refresh | Trigger a manual refresh and check history | Succeeds within the window |
| Performance | Open each page with default filters | Meets the target in the spec |
| Usability | Watch a manager find an answer without help | Answer found in the first minute |
Sign-off records who tested what and on which version. A report with no test record cannot defend its numbers in a quality meeting.
Scenario: A Dashboard That Averaged the Averages
A hospital operations team published a length-of-stay dashboard. A director compared the service line card to a query result and found a gap. The dashboard was higher by more than a day. Nobody had changed the data.
I was the business analyst on the report. I traced the gap in an hour. The report averaged a column of unit-level averages.
| Unit | Discharges | Average LOS | Total LOS days |
|---|---|---|---|
| Unit A | 100 | 3.0 | 300 |
| Unit B | 20 | 8.0 | 160 |
| Average of averages | 5.5 | ||
| Correct: total days / discharges | 120 | 3.8 | 460 |
-- Before: measure over a precomputed average column
Avg LOS = AVERAGE ( fact_unit_month[avg_los] )
-- After: the view carries additive columns, the measure divides sums
Avg LOS = DIVIDE ( SUM ( fact_unit_month[total_los_days] ), SUM ( fact_unit_month[discharges] ) )
The SQL view needed no change except two additive columns. The fix lived in the model, and the test now sits in the UAT list. Any ratio built on a precomputed ratio deserves suspicion.
The hospital, ticket number, and values are a composite from reporting projects. The arithmetic in the table is exact, and the failure is common.
Scenario: A Revenue Dashboard Every Manager Could See in Full
A revenue cycle team built a denial dashboard with row-level security so each facility manager saw only their facility. During a pre-release check, a manager reported seeing totals for every facility.
| Item | Before | After |
|---|---|---|
| Consumer access | Workspace member | App audience and viewer role |
| Row-level security result | Bypassed | Enforced |
| Security test | Desktop “View as” only | Desktop and service tests with real accounts |
| Access review | None | Quarterly, signed by the data owner |
Two lessons apply to any BI tool. Test security in the environment where users will meet it, and keep identifiers out of the model when the question does not need them. The second lesson limited the damage here.
Where Does the Logic Belong in a SQL Query to Power BI Report?
Defects hide when the same rule lives in three layers. Use the tree below to decide the home for each rule, then write it down in the spec.
What kind of rule is it?
Row-level businessrule or filter
Type fix or renamefor this report
Ratio or total thatresponds to filters
Who may seewhich rows
SQL viewTest grain and counts
Power QueryKeep steps minimal
DAX measureDivide sums, not averages
RLS + rolesTest in the service
Every branch: one home per rule, written in the spec
Performance and Effort: What Slows a Report and What It Costs
A slow report loses its audience faster than a wrong one, because people stop opening it. Most slowness comes from the model, not from Power BI itself. Check these items in order.
| Symptom | Likely cause | What to do |
|---|---|---|
| Refresh takes too long | Wide tables, unused columns, or a heavy view | Select only needed columns; aggregate in the view; check the view’s plan |
| Visuals load slowly | High-cardinality columns, complex measures, many visuals per page | Use Performance Analyzer in Desktop; simplify measures; reduce visuals |
| Transformations do not reach the database | Query folding stopped at a step | Check “View Native Query” on each step; move the logic into the view |
| Model file is large | Free-text and identifier columns loaded | Remove columns the report does not use |
| Slicers lag | Slicer on a column with millions of values | Slice on a dimension attribute instead |
Query folding means Power Query turns its steps into SQL that runs at the source. When a step breaks folding, Power Query pulls the data into the service and transforms it there. The fix is nearly always to move that step into the SQL view.
Effort and naming conventions
Plan effort by step, not by page. In my projects the SQL view and the tests took as long as the visuals. Ask for time in the estimate for profiling, view tests, security, and UAT. Teams that estimate only “build the dashboard” overrun every time.
| Item | Convention |
|---|---|
| Views | rpt.vw_[subject] for facts and rpt.vw_[dimension] for dimensions |
| Model tables | fact_[subject] and dim_[name] |
| Measures | Plain business names, grouped in a “Measures” folder |
| Report files | Name, version, and date; keep in version control or a managed library |
| Metric sheet link | Each measure description points to the metric sheet row |
Conventions look like overhead until the person who built the report leaves. Then they are the only documentation, and the team that wrote them ships fixes in an afternoon instead of a week.
Templates a Business Analyst Can Reuse
Data source request
Send this to the data owner before you build. It surfaces access and privacy questions while there is still time to answer them.
| Field | What to write |
|---|---|
| Report and purpose | Name, audience, decision supported |
| Source objects requested | View names, not base tables, where a view exists |
| Fields needed and why | Each field linked to a metric; identifiers justified or removed |
| Sensitivity | PHI, financial, or confidential; minimum necessary rationale |
| Refresh need | Daily, weekly, near real time; cutoff time |
| Account and access | Service account for refresh; named viewers or groups |
| Approver | Data owner and privacy office sign-off |
UAT checklist
| Check | Evidence | Owner | Done |
|---|---|---|---|
| Every card ties to a SQL query for the same filters | Query and screenshot pair | QA | |
| Ratios checked for two filter combinations | Hand calculation | QA | |
| Partial month and empty month behave as specified | Screenshots | QA | |
| Row-level security tested by role and with real accounts | Test log | QA and security | |
| Tooltips show definitions; no identifiers appear | Review notes | BA | |
| Refresh schedule runs after the source load | Refresh history | Report developer | |
| Users can find the answer to each page question | Observation notes | BA | |
| Sign-off recorded with version and date | Signed spec | Business owner |
Jira-style story for the report build
Role Checklists for Power BI Report Projects
Business Analyst
- Sign the metric sheet and the report spec before development starts.
- Request views and access with the data source form, not by message.
- Own the UAT checklist and the out-of-scope list.
- Record who approves definition changes after release.
Data Analyst or SQL Developer
- Build a view at a stated grain and run the grain, count, and orphan tests.
- Keep patient identifiers out of the view unless the spec requires them.
- Return additive columns, so ratios can be computed in the model.
- Version the view with the report.
Report Developer
- Model as a star with a complete date table.
- Write measures, not calculated columns, for ratios.
- Test row-level security as a viewer in the service.
- Document the storage mode decision and the refresh schedule.
QA Analyst
- Reconcile each card to SQL, and each SQL view to source.
- Test empty periods, partial months, and users outside all roles.
- Retest after every source or model change.
- Attach evidence to the ticket, never patient values.
Common Mistakes Moving From SQL Query to Power BI Report
| Mistake | Effect | Fix |
|---|---|---|
| Opening Power BI before defining the metric | Rework when definitions change | Sign the metric sheet first |
Using SELECT * from base tables |
Slow refresh and exposed columns | Connect to a governed view |
| Pasting long SQL into the connection | Hidden logic, harder tests | Move the SQL into a view |
| Averaging precomputed averages | Weighted wrongly | Divide sums by counts in a measure |
| No date table | Time comparisons fail or mislead | Add and mark a date table |
| Bidirectional or many-to-many relationships by default | Ambiguous filters, double counts | Use one-to-many, single direction |
| Calculated columns for ratios | Values do not respond to filters | Write measures |
| Sharing through workspace roles | Row-level security bypassed | Use viewer role or an app |
| Scheduling refresh before the source load ends | Stale or partial data | Schedule after the load completes |
| No refresh failure alerts | Silent stale reports | Name an owner and monitor |
Every card on my dashboard tied to SQL. Then someone filtered to one unit and the average moved the wrong way. I had tested only the unfiltered view. I now test two filter combinations for every ratio.
When I Would Use Power BI and When I Would Use Something Else
| Need | My pick | Why |
|---|---|---|
| Interactive departmental dashboard on SQL data | Power BI with Import mode | Fast, shareable, governed through the service |
| One-off answer for a single question | SQL query, then Excel | Faster than building a model |
| Pixel-exact printed report or statement | Paginated report tool | Layout control for print and PDF |
| Near-real-time operational view | DirectQuery or a purpose-built tool | Freshness outweighs speed |
| Organization already standardized on Tableau | Tableau | Support and skills already exist |
| Analysis needing statistics or scripting | Python, then publish results | Power BI shows results; it is not the best place to compute them |
Your employer’s standard usually decides the tool. The workflow in this article holds for Tableau as well: define, query, test, model, secure, publish. The which tool to master first article compares the tools by task.
Edge Cases That Break Report Workflows
| Edge case | Why it breaks the report | How to handle it |
|---|---|---|
| Source load finishes late | Refresh runs on incomplete data | Trigger refresh from the load job or add a completeness check |
| Discharge corrections after month end | Closed months change | Show “as of” time and decide when months lock |
| Time zones and daylight saving | Dates shift near midnight | Convert in the view and state the basis |
| Merged or reassigned departments | History splits across names | Use a dimension with effective dates |
| Very large fact tables | Import size and refresh time grow | Aggregate in the view or use incremental refresh |
| Users share exports | Data leaves the controlled workspace | Disable export for sensitive reports and log access |
| Personal gateways | Refresh stops when a laptop sleeps | Use an enterprise gateway and service account |
| AI-written DAX or SQL | Plausible and wrong | Reconcile output to SQL before release |
Healthcare data adds a standing constraint. The model may be small, yet it can hold identifiers that make it PHI. Classify the semantic model, limit the audience, and review exports with the privacy office.
Keeping the Report Correct After Release
A published report is a living product. Sources change, definitions change, and users find new questions. Treat the report like any other system with a support plan.
| Activity | Frequency | Owner |
|---|---|---|
| Check refresh history and failures | Daily (alerts) | Report developer |
| Rerun view tests and reconcile to source | Monthly | QA |
| Review access list and roles | Quarterly | Data owner |
| Review usage and retire unused pages | Quarterly | Business analyst |
| Handle definition changes through tickets | As needed | Business analyst and owner |
For more SQL patterns behind these views, return to the SQL for IT Analysts hub.
Questions Analysts Ask About Moving From SQL Query to Power BI Report
Can I paste my SQL query straight into Power BI?
Yes, Power BI accepts a SQL statement when you connect. For governed reports I prefer a view. The view is reviewable, testable, and reusable, and it keeps logic out of the report file.
Should the report calculate or should SQL calculate?
Let SQL apply row-level rules and return additive columns. Let DAX compute ratios and totals that respond to filters. Putting aggregates in SQL and then aggregating again in the report is how averages of averages happen.
Import or DirectQuery for a hospital dashboard?
Import fits most. Daily refresh is enough for staffing and quality views, and Import is faster. Choose DirectQuery when freshness or data size forces it, and tune the source first.
Does a business analyst need to learn DAX?
You need to read it. Knowing what a measure does lets you review the logic against your metric sheet. Writing complex DAX is a developer task on most teams.
Run This Before Your Next Report Build
Take one report in development. Pick one ratio card and two filter combinations. Compute the same ratio in SQL by dividing totals. If the card and the query disagree in either combination, fix the model before anyone sees the report.
Sources and Further Reading
- Microsoft Power BI documentation. Official guidance on storage modes, models, row-level security, gateways, and refresh.
- DAX reference. Official function reference for the measures used here.
- Microsoft Learn, Power BI row-level security and workspace roles articles (learn.microsoft.com). Confirm current role behavior for your tenant.
- U.S. Department of Health and Human Services, HIPAA minimum necessary guidance. Applies to the fields in views and models.
Ticket numbers and figures in the scenarios are illustrative composites. Table and column names follow standard Epic Clarity and vary by version. Power BI features and licensing change often, so confirm them against current documentation. Run every query in a non-production environment first.
