Checks catalog
363 checks, proven from the model. Never guessed.
Auditabi runs 363 static checks on a Power BI semantic model and its report, reading the PBIP or TMDL definition rather than the data. They cover DAX, relationships and cardinality, Power Query sources, report pages, documentation and row-level security. 20 of them are critical, meaning they break a scheduled refresh, produce wrong numbers in some filter context, or expose data; 82 are hygiene notes that are reported separately and never affect the score. The static audit is free and takes about two seconds; an optional AI review then explains root causes and writes verified rewrites.
Each page below says what is detected, why it matters and how to fix it.
DAX · 114 checks
- Arithmetic or comparison on a measure that returns textn measure(s) multiply, add, divide or compare a measure whose result is text (FORMAT, string literal,…
- Measure returning text in one branch and a number in anothern measure(s) return a string (a warning such as "Select one year") on one path and a number on another.…
- Share denominator clears a fact-table column instead of the dimensionn share measure(s) use ALL / ALLSELECTED on a column of the fact table while the same attribute exists…
- "Previous period" window overlaps the current onen measure(s) compare two DATESINPERIOD windows anchored on the same date with different lengths (for…
- Percentage label built without ×100n measure(s) format a ratio with a non-percent format and append "%" as text (FORMAT(x, "0.00") & "%").…
- Percentages and amounts summed togethern measure(s) add together values that are ratios (measures named %) and values that are amounts…
- Measure with an empty expressionn measure(s) have no expression. They show as errors in the field list and break any visual that uses…
- TOPN count does not match the measure namen measure(s) are named "Top N" but rank a different number of rows. Users trust the name.
- Division operator instead of DIVIDEn expression(s) divide with "/". A zero or BLANK denominator raises an error or returns infinity, and…
- IFERROR / ISERROR in DAXn expression(s) use IFERROR or ISERROR. Error handling forces the engine into slow row-by-row evaluation…
- FILTER(table, …) used as a CALCULATE filter argumentn expression(s) pass FILTER over a whole table to CALCULATE. The engine materialises the table instead…
- FILTER(ALL(table)) removes every filter on that tablen expression(s) rebuild a period with FILTER(ALL(DateTable), …). This drops slicers on other columns of…
- COUNT(column) used to count rowsn expression(s) count a column to obtain a row count. COUNT skips blanks and is slower than COUNTROWS.
- Column referenced without its tablehygienen expression(s) reference a column as [Column] without the table name. DAX resolves it against the…
- Measure referenced with a table prefixhygienen expression(s) reference a measure as Table[Measure]. It works, but it reads like a column and breaks…
- Deeply nested IFn expression(s) nest IF three levels or more. Nested IFs are hard to read and each branch re-evaluates…
- Iterator nested inside another iteratorn expression(s) run SUMX / AVERAGEX / FILTER inside another iterator. Cost grows with the product of the…
- USERELATIONSHIP on a pair with no inactive relationshipn expression(s) call USERELATIONSHIP on two columns that are not joined by an inactive relationship. The…
- Reference to an object that does not existn expression(s) reference a measure, table or column that is not in the model. In Power BI Desktop the…
- EARLIER instead of variablesn expression(s) use EARLIER to reach an outer row context. Variables express the same intent and are…
- Comparison with BLANK() instead of ISBLANKn expression(s) compare a value to BLANK() with = or <>. In DAX BLANK() = 0 and BLANK() = "" are TRUE,…
- ERROR() raised from a measuren expression(s) call ERROR(). The visual shows a red error box to end users instead of a blank or a…
- Same sub-expression evaluated several timesn expression(s) repeat the same CALCULATE / iterator call three or more times without a variable. Each…
- Declared variable never usedn expression(s) declare a VAR that is never referenced. Dead code that is still evaluated lazily is…
- Calculated column that calls a measuren calculated column(s) reference a measure. Each row triggers a context transition, refresh time grows…
- Calculated column with an iterator or CALCULATEn calculated column(s) run SUMX / FILTER / CALCULATE per row. On a fact table this multiplies refresh…
- Calculated columns on a fact tablen calculated column(s) live on a fact table. They are computed at refresh, stored uncompressed relative…
- Time intelligence without a marked date tableTime-intelligence functions are used in n expression(s), but no table is marked as a date table…
- TODAY() / NOW() / USERNAME() in a calculated columnn calculated column(s) use a volatile function. The value is frozen at refresh time, so "today" is…
- Share measure whose denominator does not remove any filtern measure(s) named like a share or percentage divide a measure by CALCULATE of the same measure without…
- Hardcoded identifier in a filtern expression(s) filter on a literal identifier (an ID, a code such as "D04I"). When the reference data…
- Measures scattered across many tableshygieneMeasures are defined on n different tables. Report authors have to guess where a measure lives, and…
- Long measure without variableshygienen measure(s) exceed 600 characters without a single VAR. Long single expressions are hard to debug…
- FORMAT() turns a numeric measure into textn measure(s) wrap their result in FORMAT(). The measure becomes text: it cannot be plotted, sorted…
- ALLEXCEPT applied to a fact tablen expression(s) call ALLEXCEPT on a fact table. Every column of the fact is cleared except the ones…
- ALLSELECTED inside an iteratorn expression(s) use ALLSELECTED inside SUMX, AVERAGEX or a similar iterator. ALLSELECTED depends on the…
- CALCULATE filter overrides the slicer without KEEPFILTERSn measure(s) pass a column predicate to CALCULATE on a column that is also used as a slicer attribute.…
- VALUES used as a scalar without a single-value guardn measure(s) use VALUES where a scalar is expected without checking HASONEVALUE or using SELECTEDVALUE.…
- FIRSTNONBLANK used to pick an attributen measure(s) use FIRSTNONBLANK or LASTNONBLANK to read an attribute. Those functions scan in the model's…
- Measure called inside an iterator over a large tablen expression(s) call another measure inside SUMX, AVERAGEX or MAXX over a fact table. Each row triggers…
- CALCULATE with no filter argumentn expression(s) call CALCULATE with a single argument. It only performs a context transition, which is…
- CALCULATE nested inside CALCULATEn expression(s) nest CALCULATE directly inside another CALCULATE. Filter arguments of the inner call are…
- FILTER over a table comparing a measuren expression(s) filter a whole table with a condition that calls a measure. The measure is evaluated…
- SELECTEDVALUE without an alternate resultn measure(s) call SELECTEDVALUE with one argument. When several values are selected the measure returns…
- Time intelligence anchored on a fact date columnn expression(s) pass a fact-table date column to a time-intelligence function. Those functions need a…
- Date arithmetic instead of DATEADDn expression(s) shift a period by adding or subtracting days from a date column instead of using DATEADD…
- Year-to-date on a model that looks fiscaln measure(s) use TOTALYTD or DATESYTD without a year-end date, while the model carries fiscal columns.…
- DATESBETWEEN with literal boundariesn expression(s) bound DATESBETWEEN with literal dates. The window stops moving with the calendar, so the…
- Manual year filter next to a time-intelligence functionn expression(s) combine a time-intelligence function with a manual filter on a year or month column. The…
- Number concatenated into a label without FORMATn measure(s) build a label by concatenating a numeric measure with text but never format it. The number…
- Zero substituted for BLANKn measure(s) replace BLANK with 0 with a pattern such as + 0 or IF(ISBLANK(…), 0, …). Every row of every…
- TRUE / FALSE returned where a number is expectedn measure(s) return TRUE() or FALSE() directly. A boolean measure cannot be aggregated, sorted or…
- Amounts in different currencies added togethern measure(s) add or subtract columns whose names carry different currency markers (EUR, USD, LC…).…
- Average computed as a sum divided by a count of rowsn measure(s) divide a sum by COUNTROWS of a different table. The two sides can be filtered differently,…
- DISTINCTCOUNT on a text column of a fact tablen measure(s) count distinct values of a text column on a fact table. The engine builds a dictionary of…
- COUNTROWS(DISTINCT(…)) instead of DISTINCTCOUNTn expression(s) wrap DISTINCT or VALUES in COUNTROWS. DISTINCTCOUNT expresses the same intent in one…
- SUMMARIZE used to add computed columnsn expression(s) add expressions inside SUMMARIZE. That form evaluates in a context the engine cannot…
- CROSSJOIN involving a fact tablen expression(s) cross-join a fact table. The intermediate result is the product of both row counts and…
- RANKX evaluated over a fact tablen measure(s) rank over a fact table. The expression is evaluated once per row of the fact, with a…
- Expression nested more than six levels deepn expression(s) nest function calls six levels deep or more. Nobody can predict the evaluation context…
- Same CALCULATE filter repeated in one measuren measure(s) repeat the same CALCULATE filter three times or more. The filter is evaluated each time and…
- LOOKUPVALUE where a relationship existsn calculated column(s) or measure(s) use LOOKUPVALUE between two tables that are already related.…
- RELATED pointing at an unrelated tablen expression(s) call RELATED on a table that has no relationship with the table the expression lives on.…
- RELATEDTABLE on a table with no relationshipn expression(s) call RELATEDTABLE on a table that is not related to the current one. The function…
- IF returning a number on one branch and text on the othern measure(s) return a number in one branch of IF or SWITCH and a text literal in another. The measure…
- SWITCH without a default branchn expression(s) use SWITCH with an even number of arguments, so no default result is given. Any value…
- SWITCH(TRUE()) with many branchesn expression(s) use SWITCH(TRUE()) with more than six conditions. The branches are evaluated in order…
- Business threshold written into the DAXn measure(s) compare against a literal business threshold (a target, a margin, a limit). Changing it…
- Ratio column summed instead of recomputedn measure(s) SUM a column whose name says it is a rate, a percentage or a ratio. Summing ratios is…
- Average of an averagen measure(s) average a column that already holds an average, or average another average measure. The…
- MIN or MAX applied to a key columnn measure(s) aggregate a surrogate key with MIN or MAX. The result is a technical identifier shown as a…
- Iterator over a dimension summing a fact columnn measure(s) iterate a dimension while aggregating a column of a fact table. Without a context…
- Long expression written on a single linehygienen expression(s) are longer than 200 characters on one line. Nobody reviews what they cannot read, and a…
- Variable named like a column of the modelhygienen expression(s) declare a variable whose name matches a column of the model. Readers resolve the wrong…
- Complex measure without a comment or descriptionhygienen measure(s) are longer than 300 characters, use several variables, and carry neither a comment nor a…
- Two variables holding the same expressionn expression(s) declare two variables with an identical body. One of them is dead weight, and a later…
- RETURN used without any variablehygienen expression(s) contain RETURN without a preceding VAR. The keyword adds noise without structuring…
- CROSSFILTER set to both directions in a measuren measure(s) open bidirectional filtering with CROSSFILTER. It is the right tool when a single measure…
- USERELATIONSHIP outside a CALCULATEn expression(s) call USERELATIONSHIP where it cannot apply — it is only honoured as a CALCULATE or…
- TREATAS with a different number of columnsn expression(s) call TREATAS with a table expression and a list of target columns that do not line up.…
- ISFILTERED used where HASONEVALUE is meantn measure(s) branch on ISFILTERED before reading a single value. ISFILTERED is true even when several…
- Percentage denominator clears the whole modeln measure(s) build a share with a denominator that calls ALL on several tables or on the whole model.…
- Measure referencing itselfn measure(s) reference their own name. DAX has no recursion: the model fails to refresh or the measure…
- Calculated column aggregating its own tablen calculated column(s) aggregate the table they belong to without CALCULATE. The aggregation ignores the…
- Filter on a column that is hidden from authorshygienen measure(s) filter on a column hidden from report authors. The logic is invisible from the field list,…
- Rows counted with a filter inside an iteratorn measure(s) count rows by iterating a table and testing a condition. CALCULATE with a filter argument…
- CONCATENATEX without an explicit ordern measure(s) build a list with CONCATENATEX without an order-by argument. The order then depends on the…
- FORMAT used in a calculated columnn calculated column(s) call FORMAT. The result is text stored for every row: it sorts alphabetically,…
- Ratio computed row by row inside an iteratorn measure(s) divide inside SUMX or AVERAGEX. The result is the sum or the average of per-row ratios,…
- CALCULATETABLE wrapping a plain tablehygienen expression(s) wrap a table reference in CALCULATETABLE without any filter argument. It has no effect…
- Same measure evaluated twice to test for blankn measure(s) call a measure once inside ISBLANK and again to return it. The engine evaluates it twice…
- Date column compared with a text literaln expression(s) compare a date column with a quoted literal. The comparison depends on the locale of the…
- Long literal list in an IN predicaten expression(s) test a column against a literal list of more than eight values. The list is business…
- Measure named after a currency without a currency formathygienen measure(s) carry a currency in their name but no currency symbol in their format string. Two measures…
- Division guarded by IF instead of DIVIDEn measure(s) guard a division with IF on the denominator. DIVIDE does the same in one operation, is…
- Calculated table derived from a fact tablen calculated table(s) are built from a fact table with SUMMARIZE, DISTINCT or FILTER. The table is…
- Measure depending on more than eight other measuresn measure(s) reference more than eight other measures. A change anywhere in that chain changes the…
- Join performed in DAXn expression(s) join tables in DAX with NATURALLEFTOUTERJOIN, NATURALINNERJOIN or GENERATE. Joins belong…
- Business value parsed out of a text column in DAXn calculated column(s) extract a value from text with LEFT, MID, RIGHT or SEARCH. The rule lives in the…
- Facts combined with UNION in DAXn calculated table(s) union fact tables. Column mapping is positional, so a column added to one side…
- TREATAS used where a relationship existsn measure(s) transfer a filter with TREATAS between two tables that are already related. The…
- DAX function names written in mixed casehygienen expression(s) mix upper and lower case for the same function name. Nothing breaks, but diffs and…
- Text compared with an empty stringn expression(s) compare a column with "". In Tabular an empty string and BLANK are not the same value,…
- Chain of IF where SWITCH reads betterhygienen expression(s) chain four or more IF statements on the same column. SWITCH expresses the same mapping…
- CALCULATE filtering on a measure resultn expression(s) pass a predicate on a measure to CALCULATE. A filter argument must be a table or a…
- Boolean column summedn measure(s) sum a boolean column. The result depends on how TRUE is stored and reads as a count without…
- Exchange rate written into the DAXn measure(s) multiply or divide by a literal rate next to a currency word. The rate is frozen at the day…
- TODAY() used as a filter boundaryn measure(s) filter a date column against TODAY() or NOW(). The value changes while the report is…
- Surrogate key filtered by valuen measure(s) filter a surrogate key against a literal number. Keys are regenerated by the ETL, so the…
- Measures stored on a fact tablehygienen measure(s) live on a fact table. Report authors then find measures mixed with columns, and the field…
- TOPN without a tie-breakern measure(s) use TOPN with a single order-by column. When several rows share the same value the engine…
- Hidden measure referenced by nothinghygienen hidden measure(s) are referenced by no other measure. A hidden helper that nobody calls is dead code…
- Calculated column with a context transitionn calculated column(s) wrap an aggregation in CALCULATE. Each row triggers a context transition at…
- Text compared with UPPER or LOWER on both sidesn expression(s) normalise case before comparing text. DAX comparisons are already case-insensitive, so…
Model · 63 checks
- Fact table declared on the "one" side of a relationshipn relationship(s) put the fact table on the one side and the dimension on the many side. Filters flow…
- Relationship key built from a hardcoded yearn calculated column(s) used as relationship keys are built with a literal year (DATE(2026, …)). Every…
- No table marked as date tableThe model has a calendar-like table but none is marked as a date table. Power BI then relies on auto…
- Auto date/time is enabledAuto date/time creates one hidden calendar table per date column (n detected). They inflate model size…
- Many-to-many relationshipn relationship(s) are many-to-many. Filters propagate through non-unique keys, totals can double count,…
- Relationship on a date/time columnn relationship(s) join on a dateTime column that is not the key of a marked date table. Date-time keys…
- Relationship columns with different data typesn relationship(s) join columns of different data types. Power BI converts on the fly, matches can fail…
- Relationship on text keysn relationship(s) join on string columns. Text keys need a dictionary lookup on every join; integer…
- Table with no relationshipn visible table(s) have no relationship and no measure, and are not parameter tables. They filter…
- Direct relationship between two fact tablesn relationship(s) link two fact tables directly. Facts should only relate through shared dimensions;…
- Foreign-key column visible to report authorsn column(s) on the many side of a relationship are visible. Authors drop them into visuals and get…
- Numeric key column that aggregates by defaultn numeric relationship column(s) still have summarizeBy = sum/count. Dropped in a visual they show a sum…
- Floating-point (double) columnn column(s) use the double data type. Floating point accumulates rounding errors in sums and compares…
- Columns not used anywheren column(s) are not referenced by any measure, calculated column, relationship, hierarchy, sort order,…
- Hierarchy with a single levelhygienen hierarchy(ies) have only one level. They add navigation noise without enabling drill-down.
- Mixed storage modes (composite model)The model mixes n storage mode(s) across tables. Composite models are legitimate but every cross-mode…
- Dimension joined to another dimension (snowflake)n dimension(s) are filtered by another dimension instead of standing on their own. Every extra hop costs…
- Dimension that filters nothingn table(s) look like dimensions but no fact table is on the other side of their relationships. They add…
- Fact table not connected to the date tablen fact table(s) have a date column but no relationship to the date table. Time intelligence cannot work…
- Several date tables in the same modelThe model carries n calendar-like tables. Report authors cannot tell which one drives time intelligence,…
- Bridge table connected on one side onlyn table(s) look like a bridge (two key columns, no measure) but are related on one side only. The…
- Relationship built on a calculated columnn relationship(s) join on a calculated column. The column is computed at refresh after the data loads,…
- Relationship joining columns with unrelated nameshygienen relationship(s) join two columns whose names share nothing. It may be correct, but a reader cannot…
- Several inactive relationships between the same two tablesn pair(s) of tables carry more than one inactive relationship. Only one can be activated per measure,…
- One-to-one relationshipn relationship(s) are declared one-to-one. Two tables in a one-to-one relation are one entity split in…
- Dimension without a unique keyn dimension(s) have no column marked as a key. Nothing guarantees the grain, and a duplicate row in the…
- Surrogate key visible on a dimensionhygienen surrogate key(s) are visible to report authors. They mean nothing to a business user and end up in…
- Free-text column kept on a fact tablen text column(s) on a fact table look like free text (comment, description, note, remark). Text on a…
- Amount or quantity stored as textn column(s) whose name says amount, quantity, price or rate are typed as text. They cannot be…
- Date stored as textn column(s) whose name says date are typed as text. Sorting is alphabetical, time intelligence cannot…
- Flag stored as text instead of booleann column(s) whose name says flag, is… or has… are typed as text. Each distinct spelling (Yes, YES, Y, 1)…
- Money stored as a floating-point numbern monetary column(s) use the double type. Floating point cannot represent every decimal exactly, so…
- Key stored as a floating-point numbern key column(s) use the double type. Keys must compare exactly: a floating-point key can fail to match…
- Same descriptive column repeated in several tableshygienen descriptive column name(s) exist on three tables or more. Report authors pick one at random and two…
- Identifier that aggregates by defaultn identifier column(s) have a default aggregation. Dropping one in a visual shows a sum of identifiers,…
- Hidden table whose columns are visiblehygienen hidden table(s) still expose visible columns. The table disappears from the field list but its fields…
- Table with a single column and no relationshipn table(s) hold one column and are related to nothing. They are usually leftovers of a disconnected…
- Fact table with more than thirty columnsn fact table(s) carry more than thirty columns. Wide facts usually hide attributes that belong to a…
- Many calculated columns on one tablen table(s) carry more than five calculated columns. Each one is materialised at refresh, in the order of…
- Column named exactly like its tablehygienen column(s) carry the same name as the table they belong to. Power BI then shows Table[Table], and a DAX…
- Sort-by column left visiblehygienen column(s) exist only to sort another column but are visible in the field list. Users drop them into…
- Column sorted by itselfn column(s) declare themselves as their own sort-by column. The setting does nothing and hides the fact…
- Sort-by column that does not existn column(s) are sorted by a column that is not in their table. The model fails to validate, or the sort…
- Hierarchy whose levels look invertedn hierarchy(ies) list a finer level before a coarser one (month above year, city above country).…
- Hierarchy with two levels on the same columnn hierarchy(ies) bind two levels to the same column. Drilling through it shows the same breakdown twice,…
- Table without a sourcen table(s) declare no partition. They cannot be refreshed and are usually the remains of a deleted query.
- Table split into many partitions by handn table(s) have more than six partitions declared by hand. Manual partitions have to be maintained one…
- Table mixing import and DirectQuery partitionsn table(s) carry partitions with different storage modes. Unless this is a deliberate hybrid table,…
- Small dimension left in DirectQueryn dimension(s) are in DirectQuery mode. Every slicer then queries the source, which is the slowest…
- Composite model without a dual-mode dimensionThe model mixes import and DirectQuery tables but n shared dimension(s) are import only. Every query…
- Calculation group with no itemsn calculation group(s) contain no calculation item. The group appears in the field list and does nothing.
- Measures spread over more than three tableshygieneMeasures live on n different tables. Report authors then hunt through the field list, and two measures…
- Table with no columnn table(s) declare no column. They are the remains of a deleted query and clutter the model.
- Relationship pointing from a dimension to a factn relationship(s) are declared with the dimension on the many side and the fact on the one side. Filters…
- Hidden column used by a visualn hidden column(s) are bound to a report visual. The field cannot be found in the field list, so nobody…
- Date table built from the factsn date table(s) are generated from the dates present in a fact table (CALENDARAUTO, DISTINCT or VALUES…
- Model without a single measureThe model defines no measure. Every visual then relies on implicit aggregations, which cannot be…
- Fact table with no measure built on itn fact table(s) have no measure referencing their columns. Either the table is unused, or authors are…
- Numeric column aggregated directly by a visualn numeric column(s) of a fact table are used directly in visuals while the model has explicit measures…
- Several date columns on a fact with a single relationshipn fact table(s) carry three or more date columns but only one relationship to the date table. The other…
- Calculation group without a format string expressionn calculation group(s) change what a measure computes but never adjust its format. A year-over-year item…
- Calculation item that ignores the selected measuren calculation group(s) contain items that never call SELECTEDMEASURE. Whatever measure the user picks,…
- Several calculation groups without a precedenceThe model has n calculation groups and none declares a precedence. When two of them apply to the same…
Power Query · 52 checks
- Dimension built by re-reading the fact queryn table(s) start with `Source = <fact query>` and deduplicate it. Power Query evaluates the fact query…
- Long hardcoded exclusion list in a row filtern query(ies) filter rows with five or more literal `<>` conditions in one step. The business rule…
- No-op steps in a queryhygienen query(ies) contain steps that do nothing (`Table.SelectRows(…, each true)`, empty renames). Leftovers…
- Native SQL query in Power Queryn query(ies) send hand-written SQL (Value.NativeQuery or Query=). Every later step runs in the mashup…
- Table.Buffer / List.Buffern query(ies) buffer a table or list in memory. Buffering stops query folding for every subsequent step…
- Folding-breaking step before a row filtern query(ies) apply a step that breaks query folding (index column, buffer, text function, unpivot…) and…
- Columns removed after merges or expansionsn query(ies) merge/expand first and remove columns at the end. Intermediate steps carry every column,…
- Several Changed Type stepshygienen query(ies) change column types three or more times. Repeated type conversions signal a messy pipeline…
- Hardcoded date in a row filtern query(ies) filter rows with a literal date (#date(2024, …) or a date string). The window never moves;…
- Fact table loaded from Excel or CSVn fact table(s) are loaded from Excel/CSV files. Column types are inferred, schema drifts when someone…
- No Power Query parameters although sources are literalThe model defines no parameter while n query(ies) embed literal server names or paths. Every environment…
- Table typed by hand (Enter data)n table(s) come from a pasted, base64-compressed "Enter data" step. The content is invisible in code…
- Shared query not referenced by any tablen shared expression(s) (queries with Enable load off, or helper queries) are not referenced by any…
- Custom M function applied row by rown query(ies) call a custom function inside Table.AddColumn or Table.TransformColumns. Custom functions…
- Rows sorted in Power Queryn query(ies) sort rows with Table.Sort. The engine stores rows in its own order, so the sort costs…
- Duplicates removed in Power Queryn query(ies) call Table.Distinct. Deduplication is a buffering operation: it breaks folding and forces…
- Grouping applied after a mergen query(ies) group rows after a merge. The merge materialises both tables, then the group re-reads…
- Query joining more than three tablesn query(ies) chain more than three joins. Each one buffers its right-hand table, and the refresh time…
- Every column expanded after a joinn query(ies) expand more than fifteen columns from a joined table. Most of them are never used, and each…
- Query with more than thirty stepsn query(ies) chain more than thirty steps. Nobody can hold that in their head, folding almost certainly…
- Source on a personal OneDrive siten query(ies) read from a personal OneDrive (my.sharepoint.com/personal/…). The dataset then depends on…
- Whole folder loaded as a sourcen query(ies) read every file of a folder. Any file dropped there — a backup, a temporary copy, a…
- Business rule written inside a native SQL queryn query(ies) embed a WHERE clause in a native SQL string. The rule is invisible from Power Query, cannot…
- Web.Contents without RelativePathn query(ies) build a web URL by concatenation instead of using the RelativePath and Query options. Power…
- Query steps left with their default nameshygienen query(ies) keep the generated step names (Changed Type1, Custom2, Added Custom3). The query reads as a…
- Same server and database repeated across queriesn queries repeat the same literal connection. Moving from development to production means editing every…
- Model loading from four or more kinds of sourceThe model reads from n different source technologies. Each one has its own credential, its own gateway…
- File or web source loaded without setting typesn query(ies) read a file, a web response or a folder without a single type step. Power BI then infers…
- Headers promoted without typing afterwardsn query(ies) promote the first row to headers and never set the types. Everything stays text until Power…
- Errors replaced or removed silentlyn query(ies) call Table.ReplaceErrorValues or Table.RemoveRowsWithErrors. Bad rows disappear without a…
- try … otherwise hiding a failuren query(ies) wrap a step in try … otherwise with a constant fallback. Any failure becomes a default…
- Text converted to number or date without a culturen query(ies) convert text to a number or a date without specifying the culture. The result depends on…
- Columns renamed several times in one queryhygienen query(ies) rename columns in three separate steps. Each rename breaks the next step when the source…
- File or folder source loaded with every columnn query(ies) read a file, a folder or a web response without ever restricting the columns. Every column…
- Rows filtered after the join is expandedn query(ies) filter rows after expanding a joined table. The join runs on the full set and the filter…
- Two queries with an identical bodyn query(ies) share the same M code. A fix applied to one of them leaves the other behind, and the two…
- Query name repeated in every stephygienen query(ies) prefix each step with the table name. It bloats the code and makes renaming the table a…
- Year written into a file path or a table namen query(ies) embed a year in the source path or object name. The dataset stops picking up new data on 1…
- Files combined with the generated sample functionn query(ies) use the auto-generated Combine helper. It works, but it hides three extra queries, it never…
- JSON expanded without a fixed scheman query(ies) read JSON and expand records without naming the fields. When the API adds or removes a…
- Excel source read by sheet positionn query(ies) read an Excel sheet by name or index rather than a named table. Inserting a row above the…
- CSV read without an explicit encodingn query(ies) read a CSV without setting the encoding. Accented characters turn into mojibake depending…
- Staging query loaded into the modelhygienen table(s) look like staging steps (names starting with stg, tmp, temp, raw, base) but are loaded into…
- Parameter declared without a typehygienen parameter(s) carry no type. Power BI cannot validate what is entered, and a wrong value fails deep…
- RangeStart and RangeEnd declared but not usedThe model declares the incremental-refresh parameters but n query(ies) never filter on them. Incremental…
- Long query without a single commenthygienen query(ies) run past twenty steps without one comment. The business rules applied along the way exist…
- Fuzzy matching used to join tablesn query(ies) join with fuzzy matching. The result depends on a similarity threshold: rows match today…
- Data scraped from a web pagen query(ies) read tables from a web page (Web.Page or Html.Table). The source has no contract: a layout…
- Directory or mailbox used as a sourcen query(ies) read from Active Directory, Exchange or a mailbox. Those sources carry personal data and…
- Python or R script inside a queryn query(ies) run a Python or R script. The refresh then depends on a runtime installed on the machine or…
- Fact table buffered in memoryn query(ies) call Table.Buffer on what looks like a fact table. Buffering loads the whole table into…
- Rows kept or removed by positionn query(ies) keep or remove rows by position (Table.Skip, Table.FirstN, Table.Range). Position is not a…
Report · 41 checks
- Visuals bound to tables or fields that no longer existn visual(s) reference a table or field that is not in the semantic model (renamed or deleted). Those…
- Report with many pageshygieneThe report has n visible pages. Beyond a dozen, users navigate by trial and error and every page is a…
- Page with many slicersn page(s) carry seven or more slicers. Each slicer is a query at load time and on every interaction;…
- Table or matrix with many fieldsn table/matrix visual(s) show 15 or more fields. Wide tables generate expensive queries, paginate slowly…
- Data visuals without a titlehygienen chart/table visual(s) have no title. Users cannot tell what they look at when the visual is exported,…
- Visuals aggregating raw columns (implicit measures)n visual(s) plot numeric columns directly instead of a measure. The aggregation lives in the visual: two…
- Hidden pageshygienen page(s) are hidden. Hidden pages are usually drill-through targets or abandoned drafts; drafts still…
- Visual with many local filtersn visual(s) carry five or more visual-level filters. Filters hidden in the visual override page and…
- Measures used only by hidden visualsn measure(s) are referenced exclusively by hidden visuals. They are effectively unused but escape the…
- Page left with its default namehygienen page(s) keep a generated name (Page 1, Page 2, Copy of…). The tab is the first thing a reader sees and…
- Page much taller or wider than the screenhygienen page(s) use a canvas far larger than a standard screen. Readers scroll to find visuals, and the page…
- Page holding a single visualhygienen visible page(s) contain one visual. A page per visual multiplies navigation and makes the report…
- Page filtered by something the reader cannot seen page(s) carry three or more page-level filters. Every number on the page is restricted by conditions…
- Tooltip page visible in the navigationn tooltip page(s) are not hidden. Readers land on a page designed to be shown at 300 pixels inside a…
- Page where many visuals overlapn page(s) have four or more overlapping visuals that are not driven by bookmarks. Some content is hidden…
- Visual placed outside the canvasn visual(s) sit partly or entirely outside the page. They still query the model at every interaction and…
- Data visual with no field boundn visual(s) are chart or table visuals with no field. They render as an empty placeholder in the…
- Visual bound to more than fifteen fieldsn visual(s) carry more than fifteen fields. Each one widens the query the visual sends, and a table that…
- Card visual carrying several fieldshygienen card visual(s) are bound to more than one field. A card shows one number; the extra fields silently…
- Pie or donut chart used for a breakdownhygienen pie or donut chart(s) are used in the report. Readers compare angles badly; a bar chart answers the…
- Visual with many literal colourshygienen visual(s) hard-code more than five colours instead of using the theme. Changing the palette leaves…
- Custom visuals used in the reportThe report uses n custom visual(s). They need an owner, a certification status and an update path; an…
- Visual title left as the generated onehygienen visual(s) keep the automatic title ("Sum of Amount by Country"). It describes the query, not the…
- Visual carrying more than five local filtersn visual(s) apply more than five visual-level filters. The number shown cannot be reproduced from the…
- Hidden visual that no bookmark showsn hidden visual(s) are not the target of any bookmark. They are never displayed, yet they are part of…
- Bookmark that no button triggersn bookmark(s) are referenced by no button or action. They survive every redesign and nobody dares delete…
- Button opening an external URLn button(s) navigate to a web address. The link is not versioned with the report, it can point anywhere,…
- Drill-through page without a way backn drill-through page(s) carry no back button. Readers reach them and have to use the browser to return,…
- Page where most cross-filtering is switched offn page(s) disable a large number of interactions. Clicking a visual then does nothing on most of the…
- Visible page without a mobile layouthygienen visible page(s) have no mobile layout. On a phone the reader gets the desktop canvas scaled down to an…
- Visual too small to readhygienen visual(s) are smaller than 120 by 80 pixels while carrying data. Labels are clipped and the content is…
- Slicer without a headerhygienen slicer(s) have no title. A list of values with no label leaves the reader guessing what they are…
- Visual bound to a hidden tablen visual(s) read from a table hidden in the model. The field cannot be found in the field list, so…
- One measure carrying most of the reporthygienen measure(s) appear on more than six pages. Any change to them changes the whole report at once, so they…
- Page built entirely on implicit aggregationsn page(s) contain data visuals but reference no measure at all. Every number comes from an implicit…
- Report filter applied to a fact-table columnn report or page filter(s) target a column of a fact table. Filtering the fact instead of the dimension…
- Two identical visuals on the same pagen page(s) carry two visuals of the same type bound to exactly the same fields. One of them is usually a…
- Same field sliced on many pages without syncn field(s) are used by a slicer on four pages or more. Unless the slicers are synced, a reader changes…
- Report without a custom themehygieneThe report uses the default theme while n visual(s) set colours by hand. Every new visual starts from…
- Visual attached to a page that no longer existsn visual(s) reference a page id that is not in the report. The definition is inconsistent, usually after…
- Hidden page carrying many visualsn hidden page(s) hold more than eight visuals. They are maintained, refreshed with the report and…
Governance · 11 checks
- Implicit measures are not discouragedThe model allows implicit measures (dragging a numeric column creates an ad-hoc Sum). Authors bypass the…
- Visible tables without a descriptionhygienen visible table(s) have no description. Descriptions show as tooltips in the field list and are the…
- Display folders used on some measures onlyhygieneDisplay folders exist for most measures but n visible measure(s) sit at the root. The field list mixes…
- Several calculation groupsThe model has n calculation groups. When two apply to the same measure, their precedence decides the…
- Key measure without a descriptionhygienen measure(s) are used across the report but carry no description. The tooltip in the field list is the…
- Same description reused on several measureshygienen measure(s) share an identical description with another measure. A copied description is worse than…
- Dimension attributes without a descriptionhygienen dimension(s) expose more than five visible columns with no description at all. Report authors then…
- Display folder nested more than two levels deephygienen measure(s) sit in a display folder nested three levels or more. Authors have to expand three times to…
- Display folder holding a single measurehygienen display folder(s) contain one measure. A folder per measure is the same flat list with an extra click.
- Perspective exposing tables but no measuren perspective(s) contain tables and no measure. Users switching to them find fields to drag and no…
- Culture declared with almost no translationn culture(s) are declared but translate less than a tenth of the model. Users in that language see a mix…
Security · 13 checks
- Personal data typed directly into the modeln table(s) contain e-mail addresses or people's names hardcoded in a DATATABLE / Enter-data step.…
- USERPRINCIPALNAME used for personalisation but no security rolen measure(s) personalise the report with USERPRINCIPALNAME() while the model has no RLS. The greeting is…
- Bidirectional relationship in a model with row-level securityn bidirectional relationship(s) coexist with RLS roles. Filters from RLS can leak through the reverse…
- Credential or secret embedded in a queryn query(ies) contain what looks like a password, token or API key literal. Anyone with the PBIP or the…
- Web source over plain HTTPn query(ies) call Web.Contents / Json.Document on an http:// URL. Data and any query-string secret…
- Security role without any table filtern role(s) define no table filter. Members of the role see everything, which is usually not what a role…
- RLS filter applied directly on a fact tablen role filter(s) target a fact table. The security predicate is evaluated on every fact row for every…
- User / e-mail columns in the model but no security roleThe model has n column(s) that look like user identities (e-mail, owner, user) but defines no row-level…
- Security roles that filter only part of the factsn role(s) filter some tables but leave at least one fact table unfiltered. A user in that role sees…
- Security role with an unclear namehygienen role(s) are named test, role1, temp or similar. Nobody can tell who should be in them, so nobody dares…
- Dynamic security without a mapping tablen role(s) call USERPRINCIPALNAME or USERNAME but the model has no table holding the user-to-scope…
- Sensitive personal column exposed to report authorsn visible column(s) look like personal data (birth date, national id, salary, phone, address, IBAN).…
- E-mail column exposed to report authorsn visible column(s) hold e-mail addresses. They are personal data under the GDPR and they are rarely…
Naming · 14 checks
- Technical table names visible to usershygienen visible table(s) carry technical prefixes (Dim_, Fact_, tbl_, _). Report authors and end users see…
- Technical column names visible to usersn visible column(s) use SNAKE_CASE or database-style names (NET_SHIP_AMOUNT, SOLD_TO). They show as-is…
- Object name with leading/trailing or double spaceshygienen object name(s) contain stray whitespace. They look identical to clean names but break DAX references…
- Measure and column with the same namen measure name(s) are also column names. An unqualified [Name] in DAX then depends on the row context…
- Helper measure (underscore prefix) not hiddenhygienen measure(s) named with a leading underscore — the usual convention for internal helpers — are visible…
- Inconsistent measure naming conventionhygieneMost measures follow a "GROUP - Name" convention but n do not. Mixed conventions make the field list…
- Table names mix singular and pluralhygienen visible table(s) break the model's dominant naming convention for singular and plural. It costs…
- Measure name repeating its tablehygienen measure(s) start with the name of the table they live on (Sales Sales Amount). Measures are referenced…
- Object names mixing two languageshygienen visible object(s) use French words while the rest of the model is in English, or the other way round.…
- Visible name that is an abbreviationhygienen visible object(s) are named with technical abbreviations (CUST_NM, QTY_SHP, DT_CRE). They come from…
- Object name carrying a version or a datehygienen object(s) have a version, a year or a status in their name (v2, final, new, 2024, old). The model…
- Word separators mixed across visible nameshygienen visible column(s) use a separator the rest of the model does not (underscores in a model written with…
- Object name longer than fifty charactershygienen object(s) have names longer than fifty characters. They are truncated in the field list, in legends…
- Name containing characters that need escapinghygienen object(s) contain brackets, quotes or line breaks in their name. Every DAX reference to them needs…
Formatting · 13 checks
- Month or weekday name column without sort-by columnn text column(s) hold month or weekday names without a sortByColumn. Visuals sort them alphabetically…
- Numeric measures without a format stringn visible measure(s) have no format string. Visuals show raw numbers (1234567.891) and every report…
- Percentage-named measure not formatted as percentagen measure(s) are named as a share / ratio / % but their format string is not a percentage. Users read…
- Visible numeric columns without a format stringhygienen visible numeric column(s) (amounts, quantities) have no format string, so implicit aggregations show…
- Date columns stored as dateTimen visible date column(s) are formatted with a time part (General Date, Long Time…). The engine then…
- Columns duplicated in the field list by a hierarchyhygienen column(s) on the same table are hierarchy levels and remain visible on their own. A handful is normal;…
- Measures of the same family formatted differentlyhygienen measure(s) share a business family (the same first word) but use different format strings. Two figures…
- Percentage measure formatted as an amountn measure(s) are named as a share or a rate but carry a currency or plain-number format. The reader sees…
- Monetary measure without a currency symbolhygienen monetary measure(s) are formatted as plain numbers. A reader comparing two reports cannot tell euros…
- Measure formatted with more than four decimalshygienen measure(s) show more than four decimals. The extra digits are noise in a visual and suggest a…
- Count formatted with decimalsn measure(s) count things (count, number, nombre, #) but display decimals. A visual then shows 12.00…
- Large numbers without a thousands separatorhygienen monetary or volume measure(s) are formatted without a thousands separator. Readers miscount digits,…
- Hidden measure placed in a display folderhygienen hidden measure(s) carry a display folder. The folder is never shown, so it is a note to self that the…
Structural and report checks · 42 checks
- Local file paths in Power Query sourcesPower Query sources point at a local drive (C:\Users\…, Desktop, Downloads, a personal OneDrive folder).…
- Hardcoded servers / endpoints in Power QueryServer names, database names, web endpoints or file shares are written as literals inside partition…
- Oversized M scriptsSome M scripts run to hundreds of lines with dozens of steps. They refresh slowly, rarely fold to the…
- Repeated source-query familiesSeveral partitions share the same source and the same first steps with small variations. A fix in one…
- Iterator-heavy measures with filter complexityMeasures iterate over the fact table (SUMX, FILTER, ADDCOLUMNS) with complex conditions in the row…
- Hardcoded years / static dates in DAXA year, a date or a period is written as a literal inside a measure (Calendar[Year] = 2024, DATE(2025,…
- TODAY()/NOW() assumptions in measuresA measure calls TODAY() or NOW(). The result depends on when the query runs, cannot be reproduced for a…
- Numeric columns stored as textColumns whose name or content says number (amount, quantity, id, rate) are stored as text. They cannot…
- Calculated columns without explicit typeCalculated columns have no explicit data type, so the engine infers one from the expression. A change in…
- Measures with identical DAX bodiesTwo or more measures have exactly the same DAX body under different names. They drift apart with the…
- Presentation / debug measures exposedMeasures named test, tmp, old, copy, debug or v2 are visible to report authors. They end up in a visual…
- Measure description coveragehygieneMost measures have no description. In the field list a report author only sees a name, and interprets it.
- Display-folder coveragehygieneMeasures are not organised in display folders. With more than twenty measures the field list becomes a…
- Tables with undocumented measureshygieneVisible tables carry measures with no description at all. The dataset cannot be handed over without a…
- Measures not used by the reportMeasures exist in the model but no visual, filter, other measure or RLS rule uses them. They are…
- Bidirectional relationshipsA relationship filters in both directions. It creates ambiguous filter paths, slows every query that…
- Ambiguous relationship cardinalityA relationship is declared one-to-one or many-to-many where a classic one-to-many was expected, or the…
- Star-schema topology deviations (snowflake, bridge, fact-to-fact)The model is not a star: dimensions snowflake into sub-dimensions, fact tables join each other, or a…
- Inactive relationships never activatedAn inactive relationship exists but no measure activates it with USERELATIONSHIP. It is either dead…
- Roles without filtersA security role exists with no table filter. Members of that role see the whole model — usually the…
- Permissive RLS filters (TRUE())A role filter evaluates to TRUE() on a table, which grants full access to that table. It is often a…
- Static RLS scope without identity functionRole filters use a fixed value (Region = EMEA) rather than the user identity. Every new scope needs a…
- Empty perspectiveshygieneA perspective contains no object. It appears in client tools and confuses users.
- Perspectives exposing almost everythinghygieneA perspective exposes almost the entire model. It brings no simplification and doubles the maintenance…
- Partial translation coveragehygieneTranslations exist for some objects but not others. Users in that culture see a mix of translated and…
- Visual-dense pagesA page carries far more visuals than a reader can scan, each one a query at every interaction. The page…
- Visuals with heavy local filter stateIndividual visuals carry many local filters. The logic is invisible to the reader, differs from visual…
- Mixed interaction matricesVisuals on a page have a hand-edited interaction matrix (some filter, some highlight, some none). Users…
- Overlapping visualsVisuals overlap on the canvas. Some of the content is hidden or clipped, and the reading order is…
- Hidden visual layersVisuals are hidden on the page but still refresh with every interaction. They cost queries and hide…
- Hidden-state interaction websBookmarks, buttons and hidden visuals form a web of states that only the author understands. Any edit…
- Bookmark sprawlDozens of bookmarks, many unused or duplicated, accumulate over the life of the report.
- Button navigation complexityNavigation relies on many buttons with mixed actions (bookmark, page, drill-through, URL). The report…
- Buttons pointing to missing targetsA button points to a page or bookmark that no longer exists. The click does nothing or shows an error.
- Buttons without a targetA button has a navigation action but no target selected. It looks clickable and does nothing.
- Drill interactions coupled across visualsDrill-through or drill-down settings are coupled across visuals in a way that changes what other visuals…
- Tooltip page complexityhygieneTooltip pages carry several visuals with their own filters. They are slow to appear and heavy to…
- Literal colours drifting from the themehygieneVisuals use literal colours instead of theme colours. Changing the theme leaves them behind and the…
- Pages without mobile layoutSome pages have no mobile layout while others do. On a phone, users get a mix of tailored pages and…
- Mobile layouts out of syncVisuals were added or changed on the desktop layout but not on the mobile layout. The two tell different…
- Complex report/page filter surfacehygieneReport- and page-level filters carry many conditions, some hidden from the reader. The context of every…
- Custom visual inventoryThe report uses custom visuals from AppSource or uploaded files. They need an owner, a certification…
See which of these your model fails.
Free static audit, no account, nothing stored.