clement13.net
Original photo: Pinces accrochées à un mur d’atelier, chacune entourée du contour tracé de sa place.
Description
Pinces accrochées à un mur d’atelier, chacune entourée du contour tracé de sa place.
Source
Syced · original page
Licence
CC0 Public domain
Original photo, unretouched. The background is a version of it stylised by the site.

Cross-Cutting Power BI Good Practices

Practices that apply regardless of the source (Fabric / OneLake or Managed SQL Server): star schema, naming, DAX, Power Query, RLS, performance, themes, accessibility…

Guide≈ 32 min readPartly checkedPartial checkThe main facts were checked by searching the cited sources. The items below still need checking before relying on them.To re-check
  • Rôle exact du lineageTag dans la liaison des rapports clients (règle « modifier en place ») : non documenté publiquement, issu de l'expérience du projet.
  • Syntaxe exacte du fichier de culture TMDL (cultureInfo/translations) : non confrontée à la documentation Microsoft.
  • Accès XMLA en écriture selon la licence : laissé en « à vérifier ».
Written with AI assistance; sources are cited at the bottom of the page. An error? Report it via clement13.net.
Reading
In brief
  • Practices that apply regardless of the source (Fabric / OneLake or Managed SQL Server): star schema, naming, DAX, Power Query, RLS, performance, themes, accessibility, multilingual, versioning, deployment, governance.
  • You will know how to build a readable, fast, secure, and versioned semantic model, and evolve it without breaking the reports that consume it (the lineageTag rule).
  • You will have a catalog of more than forty common pitfalls with symptom, cause, and remedy.
  • File interpretation: “OneLake + SaaS mobile apps” and “Managed SQL Server in SaaS” are defined in the folder README; this file depends on neither.
On this page
  1. 1. Guiding Principles
    1. 🟢 Novice — Five Ideas to Remember
    2. 🟡 Practical — Where Each Decision Belongs
  2. 2. Star Schema
    1. 🟢 Novice — The Principle
    2. 🟡 Practical — Concrete Rules
    3. 🔴 Expert — Edge Cases and Variants
  3. 3. Naming and Conventions
    1. 🟢 Novice — Why Name Carefully
    2. 🟡 Practical — Suggested Convention
  4. 4. DAX
    1. 🟢 Novice — The three concepts
    2. 🟡 Practical — Writing sustainable DAX
    3. 🔴 Expert — Engine, plans, and costly measures
  5. 5. Power Query and data access layer
    1. 🟢 Novice — What Power Query is for (and what it is not for)
    2. 🟡 Practical — Rules
    3. 🔴 Expert — Fabric and SaaS
  6. 6. Row-level security (RLS) and other protections
    1. 🟢 Novice — The principle
    2. 🟡 Practical — Static and dynamic RLS
    3. 🔴 Expert — Limits and interactions
  7. 7. Performance
    1. 🟢 Novice — The four levers
    2. 🟡 Practical — Method
    3. 🔴 Expert — By storage mode
  8. 8. Themes, visuals, and accessibility
    1. 🟢 Novice — Theme as a visual contract
    2. 🟡 Practical — Accessibility
    3. 🔴 Expert — Templates and visual governance
  9. 9. Multilingual and TMDL translations
    1. 🟢 Novice — Two distinct topics
    2. 🟡 Practical — Translations in the model (cultures)
    3. 🔴 Expert — Multilingual pitfalls
  10. 10. PBIP, TMDL, and Git
    1. 🟢 Novice — Why move away from .pbix
    2. 🟡 Practical — Versioning rules
    3. 🔴 Expert — Report format and drift
  11. 11. Deployment pipelines and environments
    1. 🟢 Novice — Dev, Test, Prod
    2. 🟡 Practical — Rules
    3. 🔴 Expert — Full lifecycle
  12. 12. Endorsement, catalog, and governance
    1. 🟢 Novice — Making what we share reliable
    2. 🟡 Practical — Useful governance, not bureaucratic
    3. 🔴 Expert — Self-service without chaos
  13. 13. Measures and lineageTag
    1. 🟢 Novice — Why this is a separate topic
    2. 🟡 Practical — The absolute rule
    3. 🔴 Expert — Edge cases
  14. 14. Testing and acceptance testing of numbers
    1. 🟢 Novice — Testing a report means testing numbers
    2. 🟡 Practical — Testing levels
    3. 🔴 Expert — Automate
  15. 15. Documentation
    1. 🟡 Practical — What must be documented
  16. 16. Common pitfalls
    1. 16.1 Model
    2. 16.2 DAX
    3. 16.3 Power Query and data
    4. 16.4 Security and governance
    5. 16.5 Delivery and operations
    6. 16.6 Design and adoption
  17. 17. AI contribution
  18. 18. See also

Level: 🟢→🔴

1. Guiding Principles#

🟢 Novice — Five Ideas to Remember#

  1. A report is only the storefront of a model. Quality is determined in the semantic model (tables, relationships, measures) far more than on the page.
  2. Put complexity at the source. Anything that can be calculated upstream (SQL, lakehouse) should be: a calculation done once upstream is better than doing it a thousand times at query time. Conversely, a calculation that depends on filter context belongs in DAX.
  3. One number = one definition = one measure. A KPI that exists in three versions across three reports is trust debt.
  4. Everything must be reconstructable from versioned files. If the model exists only in a .pbix on someone's machine, it does not exist.
  5. Measure before optimizing, test before delivering.

🟡 Practical — Where Each Decision Belongs#

Question Recommended layer Why
Cleanup, typing, surrogate keys Source (SQL / lakehouse) Done once, testable, reusable by other consumers
Heavy joins, fixed aggregations Source or materialized view/table The storage engine does them better than Power Query
Renaming columns for the user Source or model (translations) — never as an alias that hides the datamart name Keep column ↔ source traceability
Context-dependent measures DAX in the model Only the formula engine knows the current filter
Row-level security Model (RLS) and source if direct access Defense in depth
Formatting JSON theme + page template Consistency and single-point change
✅ Good practice

Write a one-page model charter: naming, date conventions, currency, time zone, rounding rule, definition of the top 10 KPIs. It serves as a contract between developers, and as a review aid during model review (see 11 — checklist).


2. Star Schema#

🟢 Novice — The Principle#

A star schema (Kimball, The Data Warehouse Toolkit) separates fact tables (measurable events: visits, orders, readings) and dimension tables (descriptions: customer, product, date, user). Dimensions filter facts, never the other way around, at least in the main direction: the relationship is one-to-many, “one” side = dimension, with filtering from dimension to fact.

            DIM_Date
               │
 DIM_Client ── FACT_Visite ── DIM_Produit
               │
          DIM_Utilisateur

Why: the Power BI columnar engine (VertiPaq) compresses low-cardinality columns very well and responds quickly to filters propagated from small tables to large ones; a “flat” or deep snowflake model costs memory, readability, and increases ambiguity risk.

🟡 Practical — Concrete Rules#

Rule Detail
A dedicated date table Marked as date table, continuous (no gaps), over the useful horizon; multiple roles (visit date, order date) via inactive relationships + USERELATIONSHIP, or separate date tables when the role is structural
Integer surrogate keys More compact and stable than composite text keys; no composite key on the relationship side (Power BI does not support them)
Explicit granularity Each fact table declares its grain: “one row = one visit per customer per day”
Hidden numeric fact columns Expose measures, not columns to be manually summed
Many-to-many relationships Avoid when possible; prefer a bridge table and controlled filtering
Bidirectional filtering Documented, justified exception, never the default
Unnecessary columns Removed from the model: every unused column costs memory and causes confusion
“Degenerate” dimensions A document number can remain in the fact if it has no attributes
⚠️ Pitfall

Back-filtering from facts to dimensions. Symptom: a measure counting customers gives different values depending on the user (security role) or on another visual's selection. Cause: a bidirectional relationship (or cross-security filter) lets fact-table filters restrict a dimension. Remedy: single-direction relationships, counting measures on the dimension with explicit ALL/REMOVEFILTERS when counting must not depend on security, and measure tests under each role.

🔴 Expert — Edge Cases and Variants#

  • Slowly changing dimensions (SCD). Type 2: one row per attribute version, with validity bounds. In Power BI, join the fact on the valid version's surrogate key at the time of the event, not on the natural key. Document whether the report shows “attribute at event time” or “current attribute.”
  • Multiple roles for one dimension (e.g., creator employee, seller employee): multiple relationships to the same dimension (one active) or multiple copies of the dimension; choose based on whether simultaneous filtering is needed.
  • Fact tables with different grains (weekly forecast, daily actual): shared conformed dimensions and measures that explicitly handle grain (weekly aggregation).
  • Semi-additivity (inventory, headcount): dedicated measure (LASTNONBLANK, snapshot at last date); never expose the raw column to sum.
  • Calculation groups for time variants (YTD, N-1, variance): reduce dozens of derived measures, at the cost of tooling (Tabular Editor, TMDL, or Desktop's calculation group editor) and readability that must be managed carefully.
  • Aggregations (Import aggregate tables in front of a DirectQuery table): deliver an order-of-magnitude gain on large tables, provided you test that queries do hit the aggregate.
🧪 Example

Running example: an SFA model with FACT_Visite (grain: one visit), FACT_Commande (grain: one order line), conformed dimensions DIM_Client, DIM_Produit, DIM_Utilisateur, DIM_Date. The “Coverage rate” measure counts visited customers over customers in the portfolio: the denominator must ignore filters coming from facts (REMOVEFILTERS on relevant columns), otherwise it varies by security role.


3. Naming and Conventions#

🟢 Novice — Why Name Carefully#

The name is the first piece of documentation. A self-service user selects a measure by its name; a developer finds code by its name. Column names coming from a source must remain identical to the source names: renaming in Power Query breaks traceability and complicates schema refresh. User-facing labels should go through translations (§ 9), not aliases.

🟡 Practical — Suggested Convention#

Object Convention Example
Dimension tables DIM_<Name> (or singular business name per charter) DIM_Client
Fact tables FACT_<Name> FACT_Visite
Reference/value tables LUP_<Name> LUP_StatutVisite
Parameter tables PRM_<Name> (measure or value) PRM_Devise
Measures table _Mesures (or multiple display folders) _Mesures
Measures Readable business label, with unit if ambiguous Chiffre d'affaires, Taux de couverture %
Technical measures _ prefix and hidden _Nb lignes
Columns Unchanged from source CLI_COD
Display folders Grouped by theme Ventes\Chiffre d'affaires
Pages Stable numeric prefix 01 Synthèse
Files YYYY-MM-DD only for frozen deliverables; otherwise version in the VCS tool Rapport Standard.pbip

Formatting rules: consistent casing, no obscure internal abbreviations, no characters that complicate DAX or TMDL (apostrophes in table names, for example), a unit suffix when two measures differ only by unit (%, k€), and systematically named variants (base, variant, period).

✅ Good practice

Define a naming pattern for measure variants (for example: base measure, + population filter, + fixed period, + percentage) and apply it by script. A manually derived measure will sooner or later become inconsistent with its siblings.

⚠️ Pitfall

Symptom: a user report shows “Column not found” after a rename “to make it prettier.” Cause: renaming in the model changed the reference the report used (depending on report format, references are text-based). Remedy: rename at the source with a migration plan, or keep the technical name and translate the label.


4. DAX#

🟢 Novice — The three concepts#

  • Row context: “the current row” (calculated column, iterator).
  • Filter context: all active filters (slicers, visual rows/columns, relationships, RLS).
  • Context transition: CALCULATE transforms a row context into a filter context.
Revenue = SUM ( FACT_Commande[Montant] )

Revenue N-1 =
CALCULATE ( [Revenue], SAMEPERIODLASTYEAR ( DIM_Date[Date] ) )

Prefer a measure over a calculated column as soon as the value depends on filters; a calculated column is stored (memory) and recalculated at refresh.

🟡 Practical — Writing sustainable DAX#

  1. Variables (VAR/RETURN): readability, single evaluation, easier debugging.
  2. Safe division: DIVIDE ( num, den ) rather than / to handle a zero denominator.
  3. No systematic IFERROR: it hides model errors; fix the root cause.
  4. Filters on columns, not on tables: CALCULATE ( [M], DIM_Client[Pays] = "FR" ) — a full-table filter (FILTER ( ALL ( Table ), ... )) is slow and over-constraining.
  5. Reusable base measures: each derived KPI should rely on a single base measure, not a copied formula.
  6. Use iterators sparingly: SUMX on a large table is costly; if the expression is not row-dependent, use SUM.
  7. Time intelligence with the marked date table; avoid time calculations on fact date columns.
  8. “%” measures: separate numerator, denominator, ratio to test each term.
  9. Formatting: standardized formatting (DAX Formatter or equivalent tool) makes reviews readable.
Coverage rate % =
VAR _Visites   = CALCULATE ( DISTINCTCOUNT ( FACT_Visite[ID_Client] ) )
VAR _Portefeuille =
    CALCULATE (
        DISTINCTCOUNT ( DIM_Client[ID_Client] ),
        REMOVEFILTERS ( FACT_Visite )
    )
RETURN
    DIVIDE ( _Visites, _Portefeuille )
🧪 Example

A “number of clients” defined by counting DIM_Client is 12,000 for an administrator, but 800 for a sales rep under RLS: this is normal and correct; however, a ratio whose denominator changes by role when the business did not intend it is a definition flaw. Write the business definition before the formula.

🔴 Expert — Engine, plans, and costly measures#

  • Two engines: the formula engine (evaluates DAX, single-threaded) and the storage engine (VertiPaq: xmSQL queries, multi-threaded). A slow measure is almost always one that puts heavy demand on the formula engine (nested iterators, large materializations) or triggers many callbacks.
  • Analysis tools: Performance Analyzer (in Desktop) to identify the problematic visual, DAX Studio (queries, plans, Server Timings) to understand, Tabular Editor for scripts and good practice rules (Best Practice Analyzer), and Desktop’s DAX query view.
  • Cardinality: a high-cardinality text column (IDs, second-level timestamps) is expensive; split date and time, truncate, or do not import it.
  • Cascading calculated columns: avoid them; prefer upstream calculations.
  • Inactive relationships and USERELATIONSHIP: test under RLS (security behavior and inactive relationships require explicit verification).
  • Batch-evaluated measures: the SUMMARIZECOLUMNS function is generally more efficient than combinations of ADDCOLUMNS/SUMMARIZE for query tables; think about output grain.
  • Blank values: a measure returning 0 instead of BLANK() forces display of all combinations (empty rows) and hurts performance; return BLANK() when “no data” is semantically true.
💡 Expert tip

Before optimizing a measure, isolate it: run the visual query in DAX Studio with cache cleared, record total duration / formula engine share / storage engine share / number of storage queries. After changes, compare the same four numbers. Without these values, “it’s faster” is an impression.


5. Power Query and data access layer#

🟢 Novice — What Power Query is for (and what it is not for)#

Power Query (M language) loads and prepares data: connection, typing, row filters, merge. It is not an industrial transformation engine: it works for light cleanup, not for joins across hundreds of millions of rows.

🟡 Practical — Rules#

Rule Detail
Query folding Preserve delegation of steps to the source (the SQL engine does the work). Check “View Native Query”; a non-foldable step breaks folding for subsequent steps.
Query views or tables from the exposure layer, not technical tables The source keeps the freedom to change implementation without breaking models.
Parameters Server, database, environment, date range: as parameters to switch environments without modifying queries.
One load query per table No unnecessary chained queries; disable loading for intermediate queries.
Explicit types Set types in the last step, with reliable types; watch local conversions (decimal comma/point, date formats): set the culture.
Incremental refresh Partitions by reliable modification date (RangeStart/RangeEnd), foldable filter; test folding.
No plain-text secrets Credentials in the gateway/data source, never in M.
Unnecessary columns Removed as early as possible.
let
    Source = Sql.Database(Serveur, Base),
    Faits = Source{[Schema = "dbo", Item = "FACT_Visite"]}[Data],
    Filtrage = Table.SelectRows(
        Faits,
        each [DateModification] >= RangeStart and [DateModification] < RangeEnd
    )
in
    Filtrage

🔴 Expert — Fabric and SaaS#

⚠️ Pitfall

Symptom: refresh becomes three times slower after adding a calculated column. Cause: the step breaks folding, the whole table is brought back and then processed locally. Fix: perform the transformation in the source (view) or reorder steps so foldable ones come first.


6. Row-level security (RLS) and other protections#

🟢 Novice — The principle#

RLS limits the rows visible to each user or group. It is defined in the semantic model, with roles whose expressions filter tables (generally dimensions); filters propagate to facts through relationships.

🟡 Practical — Static and dynamic RLS#

  • Static: one role per scope ([Country] = "FR"). Simple, but it proliferates.
  • Dynamic: user → scope mapping table and filter on USERPRINCIPALNAME().
-- “Representative” role on DIM_User
[Email] = USERPRINCIPALNAME ()
Good practice Why
Filter dimensions, let relationships propagate Fewer rules, better readability
Security table in the model or in the source, maintained outside Desktop Permissions change faster than reports
Flattened user hierarchies (manager → subordinates) calculated upstream Avoid costly PATH at every query
Test with “View as” with every role and a multi-role user Roles are cumulative (union) for the same user
A role with no filter expression is not “deny all” It grants access to all rows; a reader with no role assigned should in principle see no data: verify behavior
Document the role × table × expression matrix Audit and review

🔴 Expert — Limits and interactions#

  • OLS (object-level security) hides columns or tables; combine with RLS, taking client tool compatibility limits into account.
  • Workspace owners and Contributor/Member/Admin roles bypass RLS: do not assign these roles to consumers.
  • Direct Lake / SQL endpoint / DirectQuery: security must be designed at both levels (model and source); behavior depends on the identity used (SSO or fixed account) and platform feature states (to be verified, status 2026-10).
  • Embedding (reports embedded in an application): effective identity and roles are passed in the embed token; an error here is a data leak.
  • Performance: an RLS expression that references large tables or uses costly functions is paid at every query for every user.
  • Cache: visual and query caches are partitioned by security identity; many distinct roles reduce cache hit rate.
  • Sensitive data: sensitivity labels, export restrictions, no unnecessary personal data in the model (GDPR minimization).
⚠️ Pitfall

Symptom: a user sees everything in the test workspace, nothing in production. Cause: they are a member of the test workspace (therefore outside RLS), simple reader in production. Remedy: test RLS with a real reader test account, not with an admin account.


7. Performance#

🟢 Novice — The four levers#

  1. Less data: strictly necessary columns and rows only.
  2. Better model: star schema, simple relationships, suitable data types.
  3. Efficient measures (§ 4).
  4. Fewer visuals per page: each visual triggers at least one query.

🟡 Practical — Method#

Step Tool Question
Measure the page Performance Analyzer Which visual takes time? DAX query, rendering, or “other”?
Isolate the query DAX Studio (Server Timings) Formula engine or storage engine? How many storage queries?
Analyze the model VertiPaq Analyzer / model metrics Which columns weigh the most? What cardinality?
Check the source Query plans, Query Store, statistics (DirectQuery) is the native query properly indexed?
Confirm in real usage Usage logs, metrics app Are gains visible in production?

Page good practices: few visuals (around ten); dropdown slicers rather than long lists; apply page filters rather than exclusion measures; page tooltips designed to stay lightweight; bookmark buttons rather than ten nearly identical pages; avoid visuals that require tables with several thousand rows.

🧪 Example

On a 4 GB Import model, the EventTime column (timestamp to the second, high cardinality) weighed 600 MB. Split into Date (key to DIM_Date) and Time (rounded to the minute, 1,440 values), the model dropped to ~3.2 GB and load times decreased by about one fifth (illustrative figures).

🔴 Expert — By storage mode#

Mode Specific levers
Import Incremental refresh, partitioning, lower-cardinality columns, aggregations, large model format if needed
DirectQuery Columnstore indexes, up-to-date statistics, optimized views, Import aggregations, limit on parallel query count, reduce the number of visuals, avoid measures that generate nested subqueries
Composite Multi-island models: dual mode for dimensions, watch out for “limited relationships” (weakly typed)
Direct Lake Delta file compaction, V-Order, avoid DirectQuery fallback (monitor the mode actually used), consistent rowgroup size, no complex calculated columns in the model

Monitor capacity (smoothing, throttling) and concurrency: a slow page at 5 PM is often a normal page on a capacity saturated by concurrent loads. See 09 — comparison for platform trade-offs.


8. Themes, visuals, and accessibility#

🟢 Novice — Theme as a visual contract#

A JSON theme sets palette, typography, and visual styles. It guarantees consistency and allows you to change identity with a single file. A minimal theme:

{
  "name": "Academy Theme",
  "dataColors": ["#1F4E79", "#2E86AB", "#F18F01", "#C73E1D", "#6B8E23", "#7D5BA6"],
  "background": "#FFFFFF",
  "foreground": "#1B1B1B",
  "tableAccent": "#1F4E79",
  "textClasses": {
    "title":  { "fontFace": "Segoe UI Semibold", "fontSize": 14 },
    "label":  { "fontFace": "Segoe UI", "fontSize": 10 }
  }
}

🟡 Practical — Accessibility#

Requirement Implementation
Contrast Text/background contrast ratio of at least 4.5:1 for body text (WCAG 2.x guideline); verify with a tool
No information by color alone Add shape, pattern, label, or value
Alt text Provided on all information-carrying visuals; decorative visuals marked as such
Tab order Defined logically (title, filters, main visuals)
Visual titles Explicit, in business language
Font size Readable, minimum around 10 to 12 pt for labels
Keyboard navigation Tested without a mouse
Screen reader Test at least one full journey
Colorblind-friendly palette Validated palettes; avoid red/green as the only coding
Mobile Dedicated phone layout for reports consumed in the field

On mobile (Power BI app), design a dedicated vertical layout, KPI cards, few filters, and adequate touch targets; test offline if the use case requires it (offline capabilities are limited, to be verified).

🔴 Expert — Templates and visual governance#

  • Report template (.pbit) or shared base report: header, footer, navigation, theme, help pages.
  • Custom visuals: use only certified or audited visuals; a third-party visual is code executed in the report.
  • Consistency across reports: documented component library (KPI cards, tables).
  • Readability: one page = one question; titles that state the conclusion (“sales are declining in the South”) rather than the object (“Sales by region”).
  • Cognitive overload: limit the number of colors, align elements, remove noise (superfluous grids, shadows).
⚠️ Pitfall

Symptom: a “beautiful” report is not used. Cause: designed to impress, not to answer the field user's 3 questions. Remedy: start from decisions (“what should the salesperson do after looking?”), validate with a real user on a paper prototype or single page.


9. Multilingual and TMDL translations#

🟢 Novice — Two distinct topics#

  1. Translate metadata in the model (table names, columns, measures, display folders, descriptions): the viewer sees “Chiffre d'affaires” or “Revenue” depending on their client language.
  2. Translate content (dimension values, data labels): this is handled in the data (columns by language, labels table).
  3. Report text (titles, text boxes, tooltips) does not always follow model translation: plan for it separately (dynamic title measures, for example).

🟡 Practical — Translations in the model (cultures)#

The semantic model supports cultures (languages) with, for each object, a translatedCaption, translatedDescription, translatedDisplayFolder. In TMDL, they are declared in the model's cultures/ folder (one file per culture), with separate linguisticMetadata blocks.

Modèle.SemanticModel/
  definition/
    model.tmdl
    tables/
    cultures/
      fr-FR.tmdl
      en-US.tmdl
      de-DE.tmdl

Illustrative excerpt from a culture file (exact syntax must be validated against your tool version; status 2026-10, to be verified):

cultureInfo fr-FR

  translations
    model Model
      table 'FACT_Visite'
        caption: Visites
        measure 'Nb visites'
          caption: Nombre de visites
          description: Nombre de visites réalisées sur la période
          displayFolder: Activité

Good practices:

Rule Why
Single source of truth for labels (translation table in the database, or file) and script-generated cultures Consistent, reproducible translations, reviewed by a human
Each exposed object has a translation for every supported language A missing label is displayed in the source language: visible inconsistency
Source language = model authoring language Columns keep their technical names; translation provides the label
Display folders are translated too Otherwise the Fields pane mixes languages
Automated completeness checks (objects without translation) To be included in CI
Review by a native business speaker Machine translation of a business term is often wrong
Set formats (date, number, currency) by culture Users read local formats

🔴 Expert — Multilingual pitfalls#

  • Translations and report references: reports reference objects by their source name; translation does not change the reference, hence the importance of never renaming instead of translating.
  • Dynamic title measures: SELECTEDVALUE ( LUP_Libelle[Texte] ) filtered by language, or USERCULTURE() in the measure (depending on context, verify behavior in the service).
  • Sorting: translated columns must have consistent sort-by-column settings (numeric key), otherwise order changes by language.
  • Client language: the service selects culture based on browser or mobile app settings; plan a default language.
  • Multilingual content: favor a “label by language” model (labels table with LANG_COD) over Libelle_FR, Libelle_EN columns that multiply columns.
  • Removing/adding a language: documented procedure; a new language = a new culture + scripts + acceptance testing.
✅ Good practice

Treat translation like a data pipeline: labels in database → generation script → cultures/*.tmdl files → syntax validation → completeness review → publishing. A reproducible pipeline prevents drift between languages, and your teams no longer retranslate manually for every version. See the Accessibility and Multilingual matrix.


10. PBIP, TMDL, and Git#

🟢 Novice — Why move away from .pbix#

A .pbix is a binary file: you can neither compare it, nor merge it, nor review it clearly. The Power BI project (.pbip) splits a report into text files: a *.SemanticModel folder (model, in TMDL) and a *.Report folder (report definition). You can then use a version control system, compare, and perform reviews.

MonRapport.pbip
MonRapport.SemanticModel/
  definition.pbism
  definition/
    model.tmdl
    relationships.tmdl
    tables/*.tmdl
    cultures/*.tmdl
MonRapport.Report/
  definition.pbir
  definition/        (PBIR format: one file per page/visual)

🟡 Practical — Versioning rules#

Rule Detail
Version sources, not caches Exclude .pbi/cache.abf and .pbi/localSettings.json (.gitignore and .tfignore)
Encoding TMDL/PBIR files are generated by Desktop in UTF-8 without BOM: do not convert them or add BOM, Desktop rewrites them on every save
Line endings Keep the tool's defaults; do not “normalize” without testing (see Source control for data: SQL and Power BI)
Atomic commits One model change (measure, relationship) per commit, with a message that says why
Branches One branch per change, review before merge; see Branching Strategies
Merge .tmdl merges well (one measure = one block); diagramLayout.json creates low-value conflicts: accept either version
Only one editor at a time on the same model If possible: Desktop rewrites whole files
Syntax validation before opening TMDL check script in CI/pre-commit: a syntax error prevents opening the project
Report and model can be separated A report connected to a published model (shared model) rather than an “all-in-one” report, for reuse and governance

Tools: Power BI Desktop (developer mode), Tabular Editor, text editor with TMDL extension, Fabric Git integration (workspace synchronization with Git; provider limits and support to be verified, status 2026-10 — Azure DevOps cloud is the reference case; for on-premises TFS/Azure DevOps Server, plan a pipeline-based publishing path). For TFVC, see TFVC in Depth: the lack of native integration requires a manual/scripted process for check-in and deployment.

⚠️ Pitfall

Symptom: two developers modify the same model; merge produces a project that no longer opens. Cause: conflict on structural properties (two isKey columns on one table, duplicated relationships, duplicate lineageTag). Remedy: automated checks (one key per table, unique identifiers) before merge; review the TMDL diff.

🔴 Expert — Report format and drift#

  • PBIR (report in JSON files by page and visual) is the target format for report projects, better suited to diffs than the old monolithic format; it is becoming the default format for Desktop and the service (Microsoft announcement of September 1, 2026, rollout from mid- to late September 2026); the old format remains supported. Check documentation (status 2026-10).
  • Alias-based references in visual queries: a column rename leaves stale references; script renaming and validate by opening the report.
  • Manual editing of report files is possible but fragile; prefer tested scripts or the tool, and always reopen in Desktop to validate.
  • Generated files (diagram layout, editor states): exclude from review or ignore them.
  • Kit-based delivery: the published model comes with scripts (SQL, PowerShell); version model, source scripts, and documentation together, with a shared version; see CI/CD for data: foundations, datamarts, pipelines, and reports.

11. Deployment pipelines and environments#

🟢 Novice — Dev, Test, Prod#

Three workspaces (at minimum): Development, Test/Acceptance, Production. Develop in the first, validate in the second, publish in the third. Fabric/Power BI deployment pipelines automate promotion from one stage to the next, with deployment rules (parameters, data sources) specific to each stage.

🟡 Practical — Rules#

Rule Detail
Parameterize sources and environments Server, database, URL: through deployment rules or parameters, never hardcoded
Realistic but protected test data No real personal data in development; anonymized/synthetic datasets
Compare before promoting Pipeline comparison shows differences; review them
Promotion = same artifact Never rebuild for production
Permissions Developers do not write directly to production
Capacity A test capacity sized like production, otherwise performance tests are misleading
Rollback Fallback plan: previous version retained, tested procedure
Automation API/PowerShell calls from delivery pipeline (Azure DevOps, GitHub Actions) for promotion and refresh

Alternative or complement when native deployment is not enough: publish the model via XMLA (command-line tools, TMDL scripts) from a CI/CD pipeline — operation and XMLA write-access status must be verified based on your license (status 2026-10).

🔴 Expert — Full lifecycle#

  • Orchestration: database (SQL scripts) → model → reports → refresh → tests → notification. Order matters: publishing a model that references a column not yet deployed in the database breaks load.
  • Backward compatibility: consuming reports depend on the model; a breaking change (rename, measure removal) requires advance notice, a coexistence period, and the consumer list (lineage).
  • Model versions: visible version number (measure Version du modèle, “About” page), consistent with the repository tag.
  • Data schema migration: idempotent, replayable scripts with prerequisite checks.

12. Endorsement, catalog, and governance#

🟢 Novice — Making what we share reliable#

  • Promoted: “I recommend this content” (by the owner).
  • Certified: validated by authorized people according to the organization's criteria.
  • The catalog (OneLake catalog, Purview) makes it possible to find content and its lineage.

🟡 Practical — Useful governance, not bureaucratic#

Topic Recommendation
Roles Data owner, model owner, report authors, consumers, capacity administrators
Shared/certified models A small number of certified models for self-service, with lightweight reports on top
Certification criteria Documentation, successful number tests, tested security, identified owner, refresh SLA
Workspaces One owner, one purpose, naming conventions, periodic review of orphaned workspaces
Sensitivity labels Applied by default; inherited by exports
Tenant settings Export, external sharing, publish to web: decided and documented
Monitoring Activity logs, content inventory (administration APIs), actual usage
Lifecycle Archive unused reports (e.g., no views for 6 months) after prior notice

🔴 Expert — Self-service without chaos#

  • Distinguish zones: certified (controlled), team (semi-free), personal (free, unreliable). Each zone has its rules, and the user knows which one they are in.
  • Composite models on top of a certified model: allow local extension without copying, but require understanding dependency chains and security (the source model's RLS applies).
  • Governance measurement: rate of reports using a certified model, number of duplicated models, average report age, % of workspaces with an owner. See the matrix Governance, Catalog, and Self-Service.
  • Capacity: tag workloads by domain for internal chargeback; see 12 — Cost, Licensing, and Capacity.

13. Measures and lineageTag#

🟢 Novice — Why this is a separate topic#

Each object in a model (table, column, measure) carries a lineageTag: a stable identifier that lets tools recognize “the same object” despite renaming or moving. Reports and some derived artifacts (linked reports, composite models, metrics) rely on this identity.

🟡 Practical — The absolute rule#

✅ Good practice

A measure is modified in place; it is not deleted to be recreated. Deleting then recreating a measure generates a new lineageTag: client reports linked to the old identifier lose the reference, even if the name is identical.

Operational consequences:

  1. Use script instructions that replace (createOrReplace in TMDL) on the same object while preserving the lineageTag in the definition, rather than a delete then create cycle.
  2. In a TMDL diff review, watch for any lineageTag line that changes or any measure that disappears then reappears: this is a warning signal.
  3. Measure generators (variations) must reread the existing lineageTag and write it back.
  4. Breaking changes (deletion, renaming) require prior notice and a coexistence period (old measure kept, marked obsolete in its description).
  5. Keep a list of public measures (consumed outside the model) separate from internal measures.
measure 'Chiffre d''affaires' = SUM ( FACT_Commande[Montant] )
    formatString: #,0
    displayFolder: Ventes
    lineageTag: 3f8d2c1e-0b7a-4d59-9a62-5c1d7e4b9a10

🔴 Expert — Edge cases#

  • Model duplication (project copy): lineageTags are copied; two “sibling” models share identities; this may be intentional (fork) or a source of confusion in lineage — decide.
  • Branch merge: two branches that add the same measure with different lineageTags create a logical conflict, not a textual one: review is essential.
  • Refactoring tools (bulk rename): verify that they preserve tags.
  • Client reports: reports outside the team's direct scope (delivered to clients, adapted locally) are the ones that break silently: inventory them.
⚠️ Pitfall

Symptom: “after the model update, visuals in several client reports show an error on measure X.” Cause: the measure was deleted and recreated by a generation script, with a new identifier. Fix: restore the old lineageTag (find it in VCS history), rewrite the script to read/write tags, add a non-regression check (no existing tag disappears) in CI.


14. Testing and acceptance testing of numbers#

🟢 Novice — Testing a report means testing numbers#

The most costly error in BI is not an interface bug: it is a wrong number that no one challenges. So you need business acceptance testing on known numbers.

🟡 Practical — Testing levels#

Level What How
Source Volume, duplicates, orphan keys, forbidden nulls, value ranges SQL control queries, data tests (see Data Quality)
Reconciliation Model total = source total (by day, by entity) DAX query vs SQL query, defined tolerance
Model Measures on known datasets DAX query tests with fixed expected results
Security Each role sees what it should see, nothing more Role-based test matrix
Performance Load time for key pages, concurrent load Timed scenarios
Visual Rendering, labels, translations, accessibility Review checklist (see 11 — checklist)
Non-regression Fixed KPI set before/after change Automated comparison
Business acceptance testing A key user validates their reference numbers Signed acceptance report, source numbers cited
// DAX test (to run in DAX Studio or query view): difference between two definitions
EVALUATE
ROW (
    "Mesure", [Chiffre d'affaires],
    "Contrôle", CALCULATE ( SUMX ( FACT_Commande, FACT_Commande[Qte] * FACT_Commande[PrixUnitaire] ) ),
    "Ecart", [Chiffre d'affaires]
        - CALCULATE ( SUMX ( FACT_Commande, FACT_Commande[Qte] * FACT_Commande[PrixUnitaire] ) )
)

🔴 Expert — Automate#

  • Fixed reference set: expected numbers over a closed period, stored in the repository; any measure change reruns the set.
  • Executing DAX queries by command line or API from the pipeline (published model query endpoint; limits and permissions to verify).
  • Continuous data tests after each load (alert thresholds: volume variation > x% day to day).
  • Test data: representative datasets, edge cases (null values, accented characters, time zones, end of month, leap years, customers without visits).
  • Language switch tests: display the report in each language.
  • Tolerance: decide whether acceptable variance is absolute, relative, or zero (exact counts, amounts to rounding precision).
🧪 Example

A set of 25 fixed KPIs for the previous closed month; each model evolution triggers recalculation and comparison. A 0.01% variance on “margin” reveals a rounding change in a view: without testing, it would have been discovered by a client three months later.


15. Documentation#

🟡 Practical — What must be documented#

Document Content Where
Measure dictionary Name, business definition, formula, unit, owner, last review date In the model (descriptions) and exported
Column dictionary Origin (source table/column), transformation rule, quality Source extended properties, model descriptions
Model schema Star schema diagram and grains Repository, regenerated
Security matrix Roles, expressions, tests Repository
Operations guide Refresh chain, recovery, contacts, SLA Team wiki
Release notes Visible changes, breaking changes, items to test Versioned changelog
Report “About” page Version, data date, definitions, contact In the report
✅ Good practice

Place documentation as close as possible to the object: measure description in the model, extended property on the database column, README in each repository folder. Documentation separate from code ages faster than code.

💡 Expert tip

Automatically generate the dictionary from the model (TMDL or API) for each version; add this to review control: “any measure without a description is rejected.” See Metadata, lineage, and documentation.


16. Common pitfalls#

Cross-functional catalog: symptom → cause → remedy.

16.1 Model#

# Symptom Cause Remedy
1 Incorrect totals, duplicates Many-to-many relationship or non-unique key on the “one” side Deduplicate the dimension, explicit bridge
2 Values that change depending on the RLS role Bidirectional filtering / back-filtering Single direction, REMOVEFILTERS on the denominator
3 Missing dates in charts Non-continuous or unmarked date table Complete, marked date table
4 Silent time intelligence Using a date column from the fact table Use DIM_Date[Date]
5 Huge model Unnecessary columns, timestamps, long text Remove, split, reduce cardinality
6 Path ambiguity Multiple active relationships between the same tables Only one active, others inactive
7 Impossible “composite” key Power BI has no multi-column relationships Unique surrogate key
8 Two columns marked as “key” in one table Merge or manual editing Only one isKey per table, automated check
9 “(Blank)” rows in dimensions Orphan keys in facts “Unknown” row in the dimension, integrity check
10 Measure always showing 0 0 instead of BLANK() or incompatible types Return BLANK(), verify types

16.2 DAX#

# Symptom Cause Remedy
11 Slow measure Iterator over large table, full-table filter Column filters, SUM when possible
12 Different result in a table and in a card Different filter context Test with and without filters, document
13 Division by zero / without guard DIVIDE
14 IFERROR everywhere Errors hidden Address the root cause
15 Copied-and-pasted measures Duplicated formula Shared base measure
16 Total ≠ sum of rows Non-additivity of a measure Define the total explicitly (HASONEVALUE, ISINSCOPE)
17 Incorrect calculation with multiple selections SELECTEDVALUE on multi-select Plan for COUNTROWS/default value
18 Calculated column that blows up memory Stored row-by-row calculation Calculate upstream

16.3 Power Query and data#

# Symptom Cause Remedy
19 Slow refresh Broken query folding Check native query, reorder
20 Swapped months and decimals Local culture settings on the machine Set culture explicitly
21 Intermittent “Refresh failed” in SaaS API quotas or expired token Retry with increasing wait time, managed token, extraction outside Power Query
22 Column disappeared after source change Schema drift Data contract, stable exposure view, alert
23 Duplicates after incremental refresh Unreliable change column Controlled key and change date, tested partitions
24 Outdated data Refresh chain not synchronized with upstream load Trigger refresh at the end of loading
25 Numbers shifted by one day Time zones (UTC vs local) Set a convention and test it

16.4 Security and governance#

# Symptom Cause Remedy
26 RLS has no effect Testing with an admin account Real reader account
27 Leak through another access point Security applied only on the model Defense in depth by layer
28 Report shared too broadly Link sharing without review Tenant settings, periodic review
29 Orphaned content Owner left Group owner, inventory
30 Unnecessary personal data No minimization Remove unnecessary columns, hide
31 Risky third-party visual Unaudited code Certified visuals only

16.5 Delivery and operations#

# Symptom Cause Remedy
32 Client reports broken after update Measure recreated, lineageTag changed Edit in place, CI check on tags
33 “It works on my machine” Environment not parameterized Deployment parameters and rules
34 Project no longer opens TMDL syntax or merge error Syntax validation before open/commit
35 BOM added to model files Unsuitable editor or script Keep UTF-8 without BOM for project files
36 Accented characters corrupted Wrong encoding when writing scripts Explicit encoding during generation
37 Silent regression No fixed KPI set Non-regression tests
38 Capacity saturated at a fixed time Simultaneous loads and reading Stagger, split capacities, monitor
39 Different performance in test and production Unrepresentative capacity and data volume Acceptance testing in a comparable environment
40 Certified model frozen, people work around it Change lead times too long Short, visible evolution process

16.6 Design and adoption#

# Symptom Cause Remedy
41 Report not used Not part of decisions Scope by decisions, validated prototype
42 Ten versions of the same KPI No reference measure Shared model and dictionary
43 Unreadable page on mobile No phone layout Dedicated mobile layout
44 Labels mixed in three languages Incomplete translations Completeness check in CI
45 Slow report after adding “just one” visual Accumulation Visual budget per page
46 Users export to a spreadsheet Need not covered Analyze usage, provide the missing view
47 No support after delivery Operations forgotten Operations planned in the project (see 11 — checklist)
⚠️ Pitfall

The second-order pitfall: treating this catalog as a list of things to avoid without turning them into automated checks. Every line that can be tested by a script (orphan keys, missing tags, missing translations, duplicate isKey, TMDL syntax) should end up as a pipeline step.


17. AI contribution#

Use case Benefit Risk Maturity Prerequisites Guardrails
Write/explain DAX 4/5 3/5 4/5 Described model (tables, grains) Test on known dataset; do not accept an ununderstood formula
Generate measure descriptions and dictionary 4/5 2/5 4/5 Formulas and business definitions Business review; the definition comes from the business
Generate label translations 3/5 3/5 3/5 Business glossary Review by a native speaker; completeness in CI
Model review (good practice rules) 4/5 2/5 4/5 TMDL export Complements, does not replace a Best Practice Analyzer
Copilot in Power BI (summaries, natural language queries) 3/5 4/5 3/5 Clean model, descriptions, linguistic schema; adequate licenses/capacity (to be verified) Can be disabled by tenant; do not expose sensitive data; validate numbers
Generate control scripts (SQL, PowerShell) 4/5 3/5 4/5 Clear specification Read-only execution first, review
Help with performance diagnosis 3/5 3/5 3/5 DAX Studio results provided Validate measure by measure, not by plausibility

Prompt examples:

  • “Here is the business definition of the coverage rate and the model schema (tables, grains, relationships). Write the DAX measure and three test cases with expected results. Flag any definition ambiguity.”
  • “Here is a TMDL diff. List changes likely to break downstream consumer reports (rename, deletion, lineageTag change).”
🤖 AI

What AI does not replace: the business definition of a KPI, accountability for a published number, the security decision, or acceptance testing. Specific risks: plausible but wrong formula in filter context, inaccurate translation of a business term, generation that recreates measures (therefore changes lineageTag), leakage of sensitive data in a prompt, hidden debt (a “generated” model that no one understands). See also 04 — AI (OneLake) and 08 — AI (SQL Server).


18. See also#

✻ Written by Claude Sonnet 5.5