Why performance optimization matters for BI Developers
Mini task: Create one aggregation table and verify it gets used by testing a simple daily sales visual. Time the query before/after.
3) Import vs DirectQuery vs Hybrid decision
Scenario: 500M-row fact, hourly updates, fast source warehouse.
- If most reports are historical trends and do not need hourly latency, choose Import with incremental refresh.
- If users require near real-time at high granularity and the source is very fast, consider DirectQuery.
- For both: Hybrid (Import for history, DirectQuery for hot data) or a Hybrid/Incremental approach.
Rule of thumb: Start with Import + incremental refresh. Move high-latency needs to DirectQuery partitions if justified.
Mini task: Write a one-paragraph storage plan for your largest model, noting why you chose it.
4) Incremental refresh that actually works
Concept: Partition fact data by date; refresh only recent partitions.
- Add Date column in the fact (not text), ideally a full datetime.
- Parameterize RangeStart and RangeEnd.
- Filter the fact table in Power Query using RangeStart/RangeEnd.
- Configure policy (e.g., store 5 years; refresh last 7 days).
// M filter example
FilteredRows = Table.SelectRows(FactSales, each [SaleDate] >= RangeStart and [SaleDate] < RangeEnd)Mini task: Propose a refresh policy that balances size and latency (e.g., store 36 months, refresh last 3 days).
5) Faster DAX measures with variables and non-iterators
Slow pattern (iterates rows unnecessarily):
-- Slow: row-by-row iteration
[Total Sales Slow] = SUMX(FactSales, FactSales[SalesAmount])Better:
[Total Sales] = SUM(FactSales[SalesAmount])With filters and variables:
[Store Sales (This Year)] =
VAR TY = CALCULATE( SUM(FactSales[SalesAmount]), YEAR('Date'[Date]) = YEAR(TODAY()) )
RETURN TYTips:
- Prefer SUM, COUNTROWS, AVERAGE over X-iterators when possible.
- Use variables to avoid repeating expensive filters.
- Limit the number of CALCULATE modifiers and nested filters.
Mini task: Replace at least one SUMX/AVERAGEX with a base aggregator and variables where appropriate.
6) Keep query folding intact
Good: filter and type changes that fold to SQL.
// Good folding
Filtered = Table.SelectRows(FactSales, each [SaleDate] >= #date(2024,1,1))
Typed = Table.TransformColumnTypes(Filtered, {{"SalesAmount", type number}})Risky: early Table.Buffer, custom row-by-row functions, adding indexes before filters.
// Breaks folding (example)
Buffered = Table.Buffer(FactSales)
AddedIndex = Table.AddIndexColumn(Buffered, "Index", 1, 1, Int64.Type)Check folding using your tool’s native query view. If a step breaks folding, try moving it later or replace it with a foldable operation at the source.
Mini task: Reorder steps so all filters occur before non-foldable steps; confirm folding is preserved.
7) Visuals and interactions that don’t overload the model
- Limit visuals per page (aim for ~8 or fewer).
- Use simpler visuals (bars/lines) with fewer data points.
- Disable cross-highlighting when unnecessary.
- Use slicers sparingly; consider a single filter pane or parameters.
Mini task: Remove two non-essential visuals from a busy page and retest load time.
8) Monitor report load times and fix bottlenecks
- Measure visual, DAX, and query times with performance tools in your BI environment.
- Validate that aggregations are hit; if not, review mappings and relationships.
- Export query plans/logs when supported and analyze the slowest visuals first.
Mini task: Identify your three slowest visuals and note whether the delay is DAX, visual rendering, or data query time.
Drills and exercises
- Remove at least 10% of columns from a large table without losing required functionality.
- Create one aggregation table and verify it is used by a common summary visual.
- Convert one report page from 16+ visuals to 8 or fewer while preserving the story.
- Refactor two slow measures to use base aggregators and variables.
- Enable incremental refresh on a large fact and confirm only recent partitions refresh.
- Find one non-folding step and replace it with a foldable alternative.
- Measure before/after times for your slowest page and record the improvement.
Common mistakes and debugging tips
- Overusing DirectQuery: It can be slower if the source isn’t tuned. Start with Import + incremental refresh unless real-time is essential.
- Breaking query folding early: Non-foldable steps prevent pushdown. Do filters and joins first; avoid early buffering.
- Iterator-heavy measures: SUMX/AVERAGEX over large tables when SUM/AVERAGE suffice. Prefer non-iterators and variables.
- Too many visuals: Each visual is a query. Consolidate visuals and reduce data points.
- Unmapped aggregations: If queries don’t hit aggregations, check keys, relationships, and measure mappings.
- No partitioning: Full refreshes on huge tables are fragile. Use incremental refresh.
- High-cardinality text: Store as IDs; move long text to a reference table or keep it out of the model if unused.
Troubleshooting checklist
- Does your slow visual hit the aggregation table? If not, align dimensions and mapping.
- Is query folding on? Check native query; reorder steps to restore folding.
- Which component is slow: DAX compute, data query, or visual render? Optimize the dominant part first.
- Are slicers/filters exploding the result set? Limit multi-select or use fewer slicers.
Mini project: Fast Sales Performance dashboard
Build a Sales dashboard that loads in under 2 seconds on a standard page (target under typical network conditions), using:
- One Import model with incremental refresh on the fact (store 24 months, refresh last 7 days).
- One daily aggregation table mapped to the detail fact.
- Three optimized measures using variables and base aggregators.
- One page limited to 8 visuals with necessary interactions only.
- Monitoring notes: before/after timings for each visual.
Acceptance criteria
- Model size reduced by at least 20% from baseline.
- Aggregation table confirmed as query source for trend visuals.
- Incremental refresh refreshes only recent partitions.
- All visuals render within target time; no unnecessary cross-highlighting.
Subskills
- Data Model Size Reduction — Remove unused columns, lower cardinality, and set efficient data types.
- Aggregation Tables Concepts — Design and map summary tables that the engine can hit instead of detail.
- Import Versus Direct Query Strategy — Pick the right storage mode for latency, volume, and cost.
- Incremental Refresh Concepts — Partition large facts and refresh only recent data.
- Query Folding Awareness — Keep transformations pushed down to the source for speed.
- Optimizing Visuals And Interactions — Limit visuals, data points, and unnecessary cross-filtering.
- Measure Optimization Basics — Use variables and non-iterators; minimize expensive filters.
- Monitoring Report Load Times — Time visuals and DAX; find and fix the real bottleneck.
Next steps
- Apply two optimizations in a production-like report and measure the difference.
- Document your storage strategy and refresh policy for your team.
- Schedule a monthly performance review of top dashboards.
Skill exam
Take the exam to check your understanding. Everyone can take it for free. If you are logged in, your progress and results will be saved.