Deadmeasure Download
Power BI model audit

Find what your Power BI model no longer needs.

Deadmeasure reads a saved .pbix and names the measures, columns and tables that nothing reaches, with the evidence for every one it keeps. Power BI Desktop never has to be open.

Free to use. No model cap and no trial period.

Run summary illustrative sample
Model read from
DataModelSchema JSON part
Report pages
6
Measures examined
41
  • Deletable 7 referenced by nothing in the file
  • Review 2 named as text, so they are held back
  • Kept 32 evidence recorded for every one
  • Reads the saved file. Power BI Desktop is not involved.
  • Follows references through the report layout, not just the visuals.
  • The row data in the file is never read.
  • Copies your file first, verified by hash. The original is opened read-only.
01

Four things it finds, and it names each one.

Every finding is a specific object with a specific reason. Nothing is reported as a score, a grade, or a percentage of health.

  1. 01 Unused measures

    Measures that nothing references: no visual, no filter, no conditional formatting rule, no other measure's DAX, no calculated column or calculated table, no model surface. Dependencies are resolved first, so a measure that exists only to feed a dead measure is found too.

    Related deletions are grouped into one decision, and the report gives a deletion order in which no intermediate step ever leaves a measure pointing at one that is already gone.

    as reported
    7 of 41 measures (17%) are referenced by nothing in this file, carrying 412 characters of DAX.
  2. 02 Dead columns

    Columns that no visual, no surviving DAX and no relationship reaches. They are collapsed per table and sorted worst first, because a hundred column names in one flat list is not a decision anybody can make.

    Relationship endpoints are excluded on purpose. The engine joins on them at query time without any DAX naming them, so they look unreferenced and are not.

    as reported
    By table, worst first:
    - Sales: 9 of 24 columns unreferenced
    - Customer: 4 of 18 columns unreferenced
  3. 03 Fully dead tables

    When every column in a table is unreferenced, the table is flagged as such rather than left to be inferred from the column list. A whole table is one decision, not nineteen, and it is usually the largest single refresh and memory saving in the file.

    as reported
    - Store Archive 2019: 11 of 11 columns unreferenced FULLY dead
  4. 04 Functions worth extracting

    Where the same calculation has been written more than once, Deadmeasure proposes one definition to replace the copies. It handles three cases: identical bodies, the same shape over different fields, and a base calculation with filtered variants hanging off it.

    Each proposal arrives as DAX and as TMDL, with every call site written out and the edit fan-out stated, so the value is a number rather than an adjective. Names come from what the member measures and the report pages agree on, and each name carries the evidence it was derived from.

    as reported
    ShareOfTotal
    Same calculation over different fields. A change to this rule is 2 edits today, 1 after.
02

A delete list is only worth what its worst mistake costs.

Two things make this list safe to act on: the number of places it looks, and what it does when it cannot be sure.

It is built to fail in the safe direction.

A false dead is the one error you cannot absorb. Telling you to delete something that is load-bearing does not fail inside the tool. It fails in production, and a business user finds it before you do.

So the engine is asymmetric on purpose. When it can show a measure is unreachable, it says deletable. When it cannot show that either way, it says review, and it prints what it saw. Every ambiguity resolves toward keeping.

Deadmeasure is not infallible and does not claim to be. It shows its work in both directions: the evidence for every measure it kept is printed beside the list of the ones it did not, so the call can be audited instead of trusted.

  • Deletable

    Nothing in the file reaches it, and no text in the file names it. This is the list you act on.

  • Review

    Nothing references it, but its name appears as text somewhere in the file. That is exactly how a measure chosen at runtime is reached, by a SWITCH over a slicer, a field parameter, or a metric picker, and no reference analysis can see it. Held back rather than proposed.

  • Kept

    Something reaches it. The report names the reference count and where each one was found, down to the page and the visual.

Every place a measure can hide.

A reference anywhere on this list keeps a measure alive. Missing any one of them turns a live measure into a false dead, which is why the breadth is the product rather than a feature of it.

In the report

  • Visual field wells
  • Report, page and visual filters
  • Conditional formatting: fill rules
  • Conditional formatting: data bars
  • Conditional formatting: icon sets
  • Dynamic titles and labels
  • Tooltips, including page tooltips
  • Sort-by metadata
  • Bookmark state
  • Drillthrough definitions
  • Slicer state and sync groups

In the model

  • Another measure's DAX
  • Calculated column DAX
  • Calculated table DAX
  • Row-level security roles
  • Calculation items
  • KPI definitions
  • Detail rows expressions
  • Dynamic format strings
  • Relationship endpoints

The report layout is walked in full rather than at a list of known locations, so a surface a future Power BI release introduces is still picked up. Anything the walker reaches but does not recognise counts as a reference anyway and still keeps the measure.

Two things it cannot see, printed in every report.

  • Consumers outside the file

    A published semantic model can be queried from outside the .pbix: a workbook connected with Analyze in Excel, a paginated report, a composite model built on top of it, anything hitting the XMLA endpoint. None of that leaves a trace in the file. The report says so, and tells you to check workspace usage metrics before deleting a measure a business user might be querying directly.

  • Three column pins, not yet checked

    A column another column is sorted by, a column used as a level of a hierarchy, and the date column a table is marked by. All three hold a column alive without any DAX naming it. The report lists all three and asks you to confirm them before you delete a column.

03

Your rows are never read.

This is a property of how the decoder is written, not a promise in a privacy policy.

Step one of every run is a byte-identical copy of your .pbix, verified by hash. Everything after that reads the copy. Your original is opened read-only and is never opened for writing, so the safe workflow is a property of the tool rather than something you have to remember.

The .pbix is decoded on your own machine. A .pbix is a container of parts. Deadmeasure opens it, lists every part it contains, and reads two of them: the report layout, which says which fields sit on which visual, and the model metadata, which is the table, column and measure names, their DAX, and the relationships.

The large binary part is the one that holds your rows. It appears in the manifest with its size, and its bytes are never read. That is why a 400 MB file yields a few megabytes of structure. Only the model's structure is ever sent for analysis: names, types, relationships, and the report layout. The .pbix itself and every row in it stay where they are.

A test in the codebase builds a .pbix with a binary data part in it and asserts that the decoder lists the part and never marks it extracted. If that ever stopped being true, the build would fail before anything shipped.

the test
the_binary_data_model_part_is_listed_but_never_extracted
assert!(!dm.extracted, "row-data part must never be extracted");
Part manifest illustrative sample
Every part in the container, and whether its bytes were read.
PartBytesExtracted
Report/Layout1,884,214yes
DataModelSchema412,880yes
DataModel398,442,116no
Metadata3,112no
Settings128no
SecurityBindings1,024no
Report/StaticResources742,096no

DataModel is the VertiPaq store. It is 99.2 percent of this file and it is the row data. It is counted, and it is not opened.

04

What you get back.

One report, written to be read by the person who owns the model and has to defend the change. Below is the shape of one, set exactly as the tool writes it.

Deadmeasure report Illustrative sample. The model, the measure names and every figure below were written for this page. There is no customer behind it and no customer data in it.
File
quarterly-review.pbix
Model read from
DataModelSchema JSON part
Report pages
6
Measures examined
41

Measures that can be deleted

7 of 41 measures (17%) are referenced by nothing in this file, carrying 412 characters of DAX.

MeasureHiddenFolderAlso removes
Budget[Budget Prior Version]yesBudget-
Budget[Budget Variance Old]noBudgetBudget[Budget Prior Version]
Customer[Churn Flag Draft]no--
Sales[Margin Test]noScratch-
Sales[Margin Test Copy]noScratch-
Sales[Revenue 2022 Snapshot]noArchive-
Sales[Units Sold LY Draft]yes--

Grouped by decision

Each group is one judgement call. Deleting the head removes the measures beneath it, which exist only to feed it.

  • Budget[Budget Variance Old] also removes Budget[Budget Prior Version] (118 DAX characters in total)

Safe deletion order

Delete in this order and no intermediate step leaves a measure pointing at one that is already gone. Deleting them all at once is equally safe. This matters if you go one at a time or script it.

  • 1. Budget[Budget Variance Old]
  • 2. Customer[Churn Flag Draft]
  • 3. Sales[Margin Test Copy]
  • 4. Sales[Revenue 2022 Snapshot]
  • 5. Sales[Units Sold LY Draft]
  • 6. Sales[Margin Test]
  • 7. Budget[Budget Prior Version]

Unreferenced, but check before deleting

No reference to these was found either, but each one's name appears as text somewhere in the file. That is how a measure chosen at runtime is referenced, by a SWITCH over a slicer value, a field parameter, or a metric picker, and no reference analysis can see it. They are held back rather than proposed.

  • Review  Sales[Revenue per Head] : the exact text "Revenue per Head" appears in the file
  • Review  Sales[Margin Bridge Delta] : the exact text "Margin Bridge Delta" appears in the file
Show the rest of the report: the evidence for every measure it kept, dead columns by table, and the function it proposes

Measures kept, and why

The evidence for every keep, so the analysis can be audited rather than taken on faith.

MeasureRefsUsed at
Sales[Revenue]14visual field on page 'Executive Summary' (visual v1)
Sales[Gross Margin %]9conditional formatting on page 'Margin Analysis' (visual cf3)
Customer[Active Accounts]5visual field on page 'Retention' (visual v4)
Sales[Revenue LY]4another measure's DAX (Sales[Revenue YoY %])
Product[Stock Cover Days]2RLS role filter (role RegionManager)
Date[Fiscal Period Label]1sort-by on page 'Trend' (visual v7)

Six of 32 shown. A measure kept only by a row-level security role or by a conditional formatting rule is the case a spot check misses, so both are in this list.

Columns nothing reaches

24 columns are referenced by no visual, no surviving DAX, and no relationship. Columns are a bigger refresh and memory win than measures, but they are also likelier to be wanted later, so they are listed rather than proposed.

  • Store Archive 2019: 11 of 11 columns unreferenced  Fully dead
  • Sales: 9 of 24 columns unreferenced
  • Customer: 4 of 18 columns unreferenced

Consolidation into functions

Model is at compatibility level 1702. DAX user-defined functions are supported.

ShareOfTotal

Same calculation over different fields. A change to this rule is 2 edits today, 1 after. Name derived from the tokens the member measure titles agree on.

/// 2 measures share this calculation and differ only in amount.
  /// @param {COLUMNREF EXPR} amount
  FUNCTION ShareOfTotal = ( amount : COLUMNREF EXPR ) =>
      DIVIDE ( SUM ( amount ), SUM ( Sales[Net Revenue] ) )

Measures that become call sites:

  • Sales[Shipping Cost Share] = ShareOfTotal ( Sales[Shipping Cost] )
  • Sales[Discount Share] = ShareOfTotal ( Sales[Discount Amount] )

Before applying: reference parameters (COLUMNREF and MEASUREREF) must be passed EXPR, so they are evaluated inside the function rather than at the call site. The definition above already declares that. Do not change it to VAL.

Limits of this analysis

  • This analysis reads one file. If the semantic model is published, it can also be queried from outside it. Those consumers leave no trace in the .pbix.
  • 1 row-level-security role parsed. Their filter expressions are unconditional roots.
  • DAX references are found by pattern matching rather than a full grammar. Comments and string literals are removed first, so a name mentioned in a comment is not counted as a reference.

The report ends on its own limits rather than on a summary. A tool that tells you what to delete owes you the boundary of what it checked, in the same document, at the same size as everything else.

05

Get Deadmeasure.

It is free. No trial period, no model cap, no paid tier: every finding and every section of the report, on every model you run.

A Windows desktop application. Point it at a .pbix or a .pbit and it takes a verified copy, decodes the copy on your machine, and renders the audit report in its own window. Nothing is ever written back to your file.

In this build

The analysis service is not connected yet, so this build reads your file on your machine, reports the real numbers it found in it, and then shows a reference report while saying plainly that those findings are not yours. Analysis of your own model turns on when the service is connected.

Before you run it

Platform
Windows 10 and 11, x64
Package
Standalone executable, no installer
Size
10.6 MB
Code signing
Signed with an Authenticode certificate issued through Azure Trusted Signing.
SHA-256
0629df2a35e6b06589e375a2f0d5adedb66d3efb3bffa6bb6254334b3c439873

If you would rather have someone walk through a result with you, that is a support call, booked and billed separately. Running the analysis and reading the report do not need one.