Semantic Model Best Practices
Microsoft Fabric
Microsoft Fabric10 min read

Semantic Model Best Practices

Design reusable, enterprise-ready Power BI semantic models with best practices for naming, relationships, hierarchies, and calculation patterns.

By Errin O'Connor, Chief AI Architect

<h2>Semantic Model Best Practices for Power BI in 2026</h2>

<p>A well-designed semantic model (formerly called a dataset) is the single most important factor determining Power BI report performance, user experience, and long-term maintainability — every report, dashboard, paginated report, and Q&A query ultimately executes against the semantic model. Building it right from the start saves hundreds of hours of debugging, rework, and user frustration down the road.</p>

<p>After building semantic models for Fortune 500 companies across healthcare, finance, retail, and government, I have learned that the difference between a model that scales to thousands of users and one that collapses under 50 concurrent users comes down to a handful of design decisions made early in the project. These best practices are not theoretical recommendations — they are hard-won lessons from production environments serving real business users.</p>

<h2>Star Schema: The Non-Negotiable Foundation</h2>

<p>Every production semantic model should follow star schema design. This means narrow fact tables containing numeric measures and foreign keys, surrounded by wide dimension tables containing descriptive attributes used for filtering and grouping. No exceptions, no snowflake schemas, no flat denormalized mega-tables.</p>

<p>Why star schema specifically? Power BI's VertiPaq engine is optimized for star schema patterns. It compresses dimension columns with high efficiency (because dimension columns have low cardinality relative to fact tables), and it evaluates DAX queries by traversing relationships from dimensions to facts. When your model follows star schema, the engine works with rather than against your data structure.</p>

<p><strong>Common star schema violations I encounter:</strong></p>

<ul> <li><strong>Snowflake dimensions:</strong> A Product dimension that joins to a Category table that joins to a Department table. Flatten this into a single Product dimension with Category and Department as columns.</li> <li><strong>Wide fact tables:</strong> Fact tables with 50+ columns including descriptive text fields. Move descriptive columns to dimension tables; facts should contain only keys, dates, and numeric measures.</li> <li><strong>Missing date dimension:</strong> Using raw date columns from fact tables instead of a proper, dedicated date table. Create a comprehensive date table with fiscal periods, holidays, and working day flags.</li> <li><strong>Bridge tables for many-to-many:</strong> These are sometimes necessary but should be avoided when possible. Restructure your model to eliminate many-to-many relationships through role-playing dimensions or factless fact tables.</li> </ul>

<h2>Building a Proper Date Table</h2>

<p>The date table is the most important dimension in virtually every semantic model. Time intelligence functions (TOTALYTD, SAMEPERIODLASTYEAR, DATEADD) require a contiguous, complete date table marked as a date table in the model. Without it, these functions fail silently or produce incorrect results.</p>

<p><strong>Date table requirements:</strong></p>

<ul> <li>Contains one row per day with no gaps (contiguous from earliest to latest date in your data, plus buffer for future dates)</li> <li>Marked as a date table in Power BI (Table tools > Mark as date table)</li> <li>Includes fiscal calendar columns if your organization uses non-calendar fiscal years</li> <li>Contains hierarchical columns: Year > Quarter > Month > Week > Day</li> <li>Includes useful attributes: Day of Week, Is Weekend, Is Holiday, Is Working Day, Month Name, Quarter Name</li> </ul>

<p>I generate date tables in Power Query rather than DAX because Power Query handles the data types and formatting more reliably. A single date table template serves every project — customize the fiscal year start and holiday calendar per client. For time intelligence calculations that leverage this date table, see our complete guide on <a href="/blog/time-intelligence-dax-patterns-2026">time intelligence DAX patterns</a>.</p>

<h2>Measure Organization and Naming Conventions</h2>

<p>In enterprise models with 200+ measures, organization is not optional — it is essential for both developer productivity and end-user discoverability. Poor measure organization is one of the top reasons business users abandon self-service analytics and go back to asking the BI team for every request.</p>

<p><strong>Naming convention standards:</strong></p>

ElementConventionExample
Base measures[Entity] [Metric]Total Revenue, Order Count
Time intelligence[Base] [Period]Revenue YTD, Revenue SPLY
Ratios/percentages[Description] %Gross Margin %, Win Rate %
Variance[Base] [Comparison] VarRevenue YoY Var, Revenue vs Budget Var
Rankings[Entity] RankProduct Revenue Rank
Boolean/flagsIs [Condition]Is Active Customer, Is Current Period

<p><strong>Measure tables:</strong> Create dedicated display folders (or a separate "_Measures" table with no data) to organize measures by business domain: Financial Measures, Sales Measures, HR Measures, Operational Measures. This prevents measures from scattering across fact tables and makes them easy to find in the field list.</p>

<p><strong>Documentation:</strong> Every measure should have a Description property explaining what it calculates, its business definition, and any important caveats. These descriptions surface in Q&A, in Excel's field list, and in Power BI's field tooltips. A measure named "Adjusted Revenue" means nothing without context — the description should explain which adjustments are applied and why.</p>

<h2>Relationship Design and Optimization</h2>

<p>Relationships define how tables connect and how filter context propagates through your model. Getting relationships right is fundamental to correct DAX calculations.</p>

<p><strong>Relationship rules:</strong></p>

<ul> <li><strong>Single direction by default:</strong> Use single-direction cross-filtering (dimension to fact). Bidirectional filtering introduces ambiguity, performance overhead, and unexpected results. Only enable bidirectional when absolutely required, and document why.</li> <li><strong>One active path between any two tables:</strong> If multiple paths exist between tables, only one can be active. Use USERELATIONSHIP in DAX to traverse inactive relationships for specific calculations.</li> <li><strong>Prefer integer keys:</strong> Relationships on integer columns perform better than relationships on text columns. Convert natural keys to integer surrogate keys in Power Query.</li> <li><strong>Handle missing keys:</strong> Add an "Unknown" row to dimension tables so that fact records with missing foreign keys still resolve through the relationship. This prevents orphaned fact rows that disappear from filtered reports.</li> </ul>

<h2>Optimizing for Import Mode Performance</h2>

<p>Import mode loads data into VertiPaq's in-memory columnar store, where compression and query performance depend on data characteristics. These optimization techniques directly reduce model size and improve query speed:</p>

<p><strong>Reduce cardinality:</strong> VertiPaq compresses columns based on the number of distinct values. A column with 10 distinct values compresses far better than one with 10 million. Reduce cardinality by:</p>

<ul> <li>Splitting DateTime columns into separate Date and Time columns (Date has ~365 values/year vs. 86,400 seconds)</li> <li>Rounding decimal values to necessary precision (do you really need 15 decimal places for revenue?)</li> <li>Removing high-cardinality columns that are not used in reports (GUIDs, freetext notes, internal IDs)</li> </ul>

<p><strong>Remove unnecessary columns:</strong> Every column in your model consumes memory, even if no report uses it. Audit your model quarterly and remove columns that do not appear in any visual, filter, slicer, or DAX calculation. I routinely reduce model size by 30-40% through column pruning alone.</p>

<p><strong>Use calculated columns sparingly:</strong> Calculated columns consume memory and increase refresh time. If a column can be computed in Power Query (source query), do it there instead. Reserve calculated columns for scenarios that genuinely require DAX evaluation context, such as columns that reference measures or need row context with related table lookups.</p>

<h2>Row-Level Security Design</h2>

<p>For enterprise deployments, row-level security (RLS) ensures users see only the data they are authorized to access. Designing RLS correctly from the start is far easier than retrofitting it after deployment.</p>

<p><strong>RLS implementation approaches:</strong></p>

<ul> <li><strong>Static roles:</strong> Define fixed filter expressions per role (e.g., [Region] = "East"). Simple but requires a new role for every filter value.</li> <li><strong>Dynamic RLS:</strong> Use USERPRINCIPALNAME() to filter data based on the authenticated user's email address. Requires a mapping table (SecurityTable) that maps email addresses to the data they can access. This scales to thousands of users without creating thousands of roles.</li> <li><strong>Hybrid approach:</strong> Combine static roles for broad categories (Regional Manager, Executive) with dynamic filtering within each role. This provides both organizational structure and user-specific filtering.</li> </ul>

<p>Test RLS thoroughly using the "View as Role" feature in Power BI Desktop. Verify both that authorized users see correct data AND that unauthorized users are properly restricted. I have seen implementations where RLS worked for direct table access but failed when DAX measures traversed relationships — always test with realistic report visuals, not just table scans.</p>

<h2>Incremental Refresh for Large Models</h2>

<p>For models with millions of rows, incremental refresh is essential. Rather than reloading the entire dataset on every refresh, incremental refresh reloads only recent partitions (e.g., last 30 days) while retaining historical partitions unchanged.</p>

<p><strong>Configuration requirements:</strong></p> <ul> <li>Your data source query must include RangeStart and RangeEnd parameters of DateTime type</li> <li>The source must support query folding for the date filter (so the filter pushes to the source rather than loading all data then filtering)</li> <li>Define the incremental refresh policy: how much history to keep (e.g., 3 years), how much to refresh (e.g., 30 days)</li> </ul>

<p>Incremental refresh reduced one client's 4-hour daily refresh to 12 minutes by refreshing only the current month's partition against a 3-year, 500-million-row fact table. The savings compound — less compute, shorter refresh windows, and reduced source system load.</p>

<h2>Composite Models and DirectQuery Considerations</h2>

<p>Composite models combine import and DirectQuery tables in a single semantic model. This is valuable when some data needs real-time freshness (DirectQuery to the source) while other data benefits from import mode performance.</p>

<p><strong>When to use composite models:</strong></p> <ul> <li>Large fact tables that exceed import capacity limits — use DirectQuery for the fact, import for dimensions</li> <li>Real-time operational dashboards where certain metrics must reflect current state</li> <li>Connecting to certified shared semantic models (chaining) while adding local import tables for supplementary data</li> </ul>

<p>For Fabric environments, <a href="/blog/power-bi-direct-lake-mode-guide-2026">Direct Lake mode</a> largely eliminates the need for composite models by providing near-import performance with DirectQuery-like freshness from Delta tables.</p>

<h2>Model Validation Checklist</h2>

<p>Before publishing any semantic model to production, run through this validation checklist:</p>

<ul> <li>All tables follow star schema (facts and dimensions properly separated)</li> <li>Date table exists, is marked, and is contiguous</li> <li>No bidirectional relationships unless documented and justified</li> <li>All measures have descriptions</li> <li>Measures are organized in display folders</li> <li>RLS roles are configured and tested</li> <li>No unused columns remain in the model</li> <li>High-cardinality columns have been optimized or removed</li> <li>Incremental refresh is configured for tables over 1 million rows</li> <li>Model size is within capacity limits with room for growth</li> </ul>

<p>For DAX calculations that build on a solid semantic model foundation, see our guides on <a href="/blog/essential-dax-patterns">essential DAX patterns</a> and <a href="/blog/power-bi-governance-framework">governance frameworks</a> for standardizing model design across your organization.</p>

Frequently Asked Questions

What is the difference between semantic models and datasets?

They are the same thing. Microsoft renamed "datasets" to "semantic models" to better reflect their purpose of providing business meaning and context to data, not just storing it.

Should every report have its own semantic model?

No, best practice is to create shared semantic models that serve multiple reports. This ensures consistency and reduces maintenance. Only create separate models when requirements are fundamentally different.

Microsoft FabricSemantic ModelData ModelingEnterprise

Industry Solutions

See how we apply these solutions across industries:

Need Help With Power BI?

Our experts can help you implement the solutions discussed in this article.

Ready to Transform Your Data Strategy?

Get a free consultation to discuss how Power BI and Microsoft Fabric can drive insights and growth for your organization.