[ { "ID": "DAX_COLUMNS_FULLY_QUALIFIED", "Name": "Column references should be fully qualified", "Category": "DAX Expressions", "Description": "Using fully qualified column references makes it easier to distinguish between column and measure references, and also helps avoid certain errors.", "Severity": 2, "Scope": "Measure, CalculatedColumn, CalculatedTable, KPI", "Expression": "DependsOn.Any(Key.ObjectType = \"Column\" and Value.Any(not FullyQualified))", "CompatibilityLevel": 1200 }, { "ID": "DAX_DIVISION_COLUMNS", "Name": "Avoid division (use DIVIDE function instead)", "Category": "DAX Expressions", "Description": "Calculated Columns, Measures or Calculated Tables should not use the division symbol in their expressions (/) unless the denominator is a constant value. Instead, it is advised to always use the DIVIDE(,) function.", "Severity": 3, "Scope": "Measure, CalculatedColumn, CalculatedTable", "Expression": "Tokenize().Any(\n Type = DIV and\n Next.Type <> INTEGER_LITERAL and\n Next.Type <> REAL_LITERAL\n)", "CompatibilityLevel": 1200 }, { "ID": "DAX_MEASURES_UNQUALIFIED", "Name": "Measure references should be unqualified", "Category": "DAX Expressions", "Description": "Using unqualified measure references makes it easier to distinguish between column and measure references, and also helps avoid certain errors.", "Severity": 2, "Scope": "Measure, CalculatedColumn, CalculatedTable, KPI", "Expression": "DependsOn.Any(Key.ObjectType = \"Measure\" and Value.Any(FullyQualified))", "CompatibilityLevel": 1200 }, { "ID": "DAX_TODO", "Name": "Revisit TODO expressions", "Category": "DAX Expressions", "Description": "Objects with an expression containing the word \"TODO\" (typically as a comment), should most likely be revisited.", "Severity": 1, "Scope": "Measure, Partition, CalculatedColumn, CalculatedTable", "Expression": "Expression.IndexOf(\"TODO\", StringComparison.OrdinalIgnoreCase) >= 0", "CompatibilityLevel": 1200 }, { "ID": "APPLY_FORMAT_STRING_COLUMNS", "Name": "Provide format string for visible numeric columns", "Category": "Formatting", "Description": "Visible numeric columns should have their Format String property assigned", "Severity": 2, "Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn", "Expression": "IsVisible\nand string.IsNullOrWhitespace(FormatString)\nand (DataType = \"Int64\" or DataType = \"DateTime\" or DataType = \"Double\" or DataType = \"Decimal\")\n", "CompatibilityLevel": 1200 }, { "ID": "APPLY_FORMAT_STRING_MEASURES", "Name": "Provide format string for visible numeric measures", "Category": "Formatting", "Description": "Visible measures should have their Format String property assigned", "Severity": 2, "Scope": "Measure", "Expression": "IsVisible\nand string.IsNullOrWhitespace(FormatString)\nand (DataType = \"Int64\" or DataType = \"DateTime\" or DataType = \"Double\" or DataType = \"Decimal\")", "CompatibilityLevel": 1200 }, { "ID": "META_AVOID_FLOAT", "Name": "Do not use floating point data types", "Category": "Metadata", "Description": "Floating point datatypes can cause unexpected results when evaluating values close to 0. Use Currency / Fixed Decimal Number (decimal) instead.", "Severity": 3, "Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn", "Expression": "DataType = \"Double\"", "FixExpression": "DataType = DataType.Decimal", "CompatibilityLevel": 1200 }, { "ID": "META_SUMMARIZE_NONE", "Name": "Don't summarize numeric columns", "Category": "Metadata", "Description": "Set the SummarizeBy property of all visible numeric columns to \"None\", to avoid unintentional summarization in client tools. Create measures for columns that are supposed to be summarized.", "Severity": 1, "Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn", "Expression": "IsVisible\nand SummarizeBy <> \"None\"\nand (DataType = \"Double\" or DataType = \"Decimal\" or DataType = \"Int64\")", "FixExpression": "SummarizeBy = AggregateFunction.None", "CompatibilityLevel": 1200 }, { "ID": "LAYOUT_ADD_TO_PERSPECTIVES", "Name": "Add objects to perspectives", "Category": "Model Layout", "Description": "Visible tables, columns, measures and hierarchies should be assigned to at least one perspective, if the Tabular Model uses perspectives. Otherwise, the objects will only be visible when connecting directly to the model.", "Severity": 1, "Scope": "Table, Measure, Hierarchy, DataColumn, CalculatedColumn, CalculatedTableColumn", "Expression": "IsVisible\nand Model.Perspectives.Any()\nand not InPerspective.Any(it)", "CompatibilityLevel": 1200 }, { "ID": "LAYOUT_COLUMNS_HIERARCHIES_DF", "Name": "Organize columns and hierarchies in display folders", "Category": "Model Layout", "Description": "Tables with more than 10 visible columns and/or hierarchies should have them organized in display folders for improved usability.", "Severity": 1, "Scope": "Table", "Expression": "Columns.Count(IsVisible and string.IsNullOrEmpty(DisplayFolder)) +\nHierarchies.Count(IsVisible and string.IsNullOrEmpty(DisplayFolder))\n> 10", "CompatibilityLevel": 1200 }, { "ID": "LAYOUT_HIDE_FK_COLUMNS", "Name": "Hide foreign key columns", "Category": "Model Layout", "Description": "Columns used on the Many side of a relationship should be hidden, as the related (dimension) table is likely the best place to apply a filter context.", "Severity": 1, "Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn", "Expression": "IsVisible\nand Model.Relationships.Any(FromColumn = outerIt)", "FixExpression": "IsHidden = true", "CompatibilityLevel": 1200 }, { "ID": "LAYOUT_LOCALIZE_DF", "Name": "Translate Display Folders", "Category": "Model Layout", "Description": "Display Folder translations should be assigned for objects where the base DisplayFolder property has been assigned. Otherwise, users connecting to the model using a specific Culture will not see the Display Folder structure.", "Severity": 1, "Scope": "Measure, Hierarchy, DataColumn, CalculatedColumn, CalculatedTableColumn", "Expression": "IsVisible\nand not string.IsNullOrEmpty(DisplayFolder)\nand Model.Cultures.Any(it.Name != Model.Culture and string.IsNullOrEmpty(outerIt.TranslatedDisplayFolders[it]))", "FixExpression": "TranslatedDisplayFolders.Reset()", "CompatibilityLevel": 1200 }, { "ID": "LAYOUT_MEASURES_DF", "Name": "Organize measures in display folders", "Category": "Model Layout", "Description": "Tables with more than 10 visible measures should have them organized in display folders for improved usability", "Severity": 1, "Scope": "Table", "Expression": "Measures.Count(IsVisible and string.IsNullOrEmpty(DisplayFolder)) > 10", "CompatibilityLevel": 1200 }, { "ID": "TRANSLATE_DESCRIPTIONS", "Name": "Translate Object Descriptions", "Category": "Model Layout", "Description": "When the model contains one or more cultures, all objects that have descriptions applied, should also have translated descriptions applied.", "Severity": 1, "Scope": "Model, Table, Measure, Hierarchy, Level, Perspective, DataColumn, CalculatedColumn, CalculatedTable, CalculatedTableColumn", "Expression": "not string.IsNullOrEmpty(Description) and Model.Cultures.Any(it.Name != Model.Culture and string.IsNullOrEmpty(outerIt.TranslatedDescriptions[it]))", "CompatibilityLevel": 1200 }, { "ID": "TRANSLATE_HIDEABLE_OBJECT_NAMES", "Name": "Translate Visible Object Names", "Category": "Model Layout", "Description": "When the model contains one or more cultures, all visible objects should have a name translation provided in that culture.", "Severity": 1, "Scope": "Table, Measure, Hierarchy, DataColumn, CalculatedColumn, CalculatedTable, CalculatedTableColumn", "Expression": "IsVisible and Model.Cultures.Any(it.Name != Model.Culture and string.IsNullOrEmpty(outerIt.TranslatedNames[it]))", "CompatibilityLevel": 1200 }, { "ID": "TRANSLATE_HIERARCHY_LEVEL_NAMES", "Name": "Translate Hierarchy Levels", "Category": "Model Layout", "Description": "When the model contains one or more cultures, all levels on visible hirearchies should have their a translation applied to their name in all cultures.", "Severity": 1, "Scope": "Level", "Expression": "Hierarchy.IsVisible and Model.Cultures.Any(it.Name != Model.Culture and string.IsNullOrEmpty(outerIt.TranslatedNames[it]))", "CompatibilityLevel": 1200 }, { "ID": "TRANSLATE_OTHER_NAMES", "Name": "Translate Perspectives", "Category": "Model Layout", "Description": "When the model contains one or more cultures, the model object and any perspectives in the model should have a translated name assigned in all cultures.", "Severity": 1, "Scope": "Model, Perspective", "Expression": "Model.Cultures.Any(it.Name != Model.Culture and string.IsNullOrEmpty(outerIt.TranslatedNames[it]))", "CompatibilityLevel": 1200 }, { "ID": "NO_CAMELCASE_COLUMNS_HIERARCHIES", "Name": "Avoid CamelCase on visible columns and hierarchies", "Category": "Naming Conventions", "Description": "Visible columns and hierarchies should not use CamelCase in their names, unless translations are applied", "Severity": 2, "Scope": "Hierarchy, DataColumn, CalculatedColumn, CalculatedTableColumn", "Expression": "IsVisible \nand RegEx.IsMatch(Name, \"[A-Z]([A-Z0-9]*[a-z][a-z0-9]*[A-Z]|[a-z0-9]*[A-Z][A-Z0-9]*[a-z])[A-Za-z0-9]*\") \nand not Name.Contains(\" \") \nand (Model.Cultures.Count = 0 or TranslatedNames.Any(it = \"\" or it = outerIt.Name))", "CompatibilityLevel": 1200 }, { "ID": "NO_CAMELCASE_MEASURES_TABLES", "Name": "Avoid CamelCase on visible measures and tables", "Category": "Naming Conventions", "Description": "Visible measures and tables should not use CamelCase in their names, unless translations are applied", "Severity": 2, "Scope": "Table, Measure, CalculatedTable", "Expression": "IsVisible \nand RegEx.IsMatch(Name, \"[A-Z]([A-Z0-9]*[a-z][a-z0-9]*[A-Z]|[a-z0-9]*[A-Z][A-Z0-9]*[a-z])[A-Za-z0-9]*\") \nand not Name.Contains(\" \") \nand (Model.Cultures.Count = 0 or TranslatedNames.Any(it = \"\" or it = outerIt.Name))", "CompatibilityLevel": 1200 }, { "ID": "RELATIONSHIP_COLUMN_NAMES", "Name": "Names of columns in relationships should be the same", "Category": "Naming Conventions", "Description": "When a single relationship exists between two tables, the columns on both sides of the relationship must have the same name. When multiple relationships exist between two tables, the name of the FromColumn must end with the name of the ToColumn (for example OrderDateKey, ShipDateKey, DueDateKey, etc.)", "Severity": 2, "Scope": "Relationship", "Expression": "(Model.Relationships.Count(FromTable = OuterIt.FromTable and ToTable = OuterIt.ToTable) = 1 and FromColumn.Name <> ToColumn.Name) or\n(Model.Relationships.Count(FromTable = OuterIt.FromTable and ToTable = OuterIt.ToTable) > 1 and not FromColumn.Name.EndsWith(ToColumn.Name))", "CompatibilityLevel": 1200 }, { "ID": "UPPERCASE_FIRST_LETTER_COLUMNS_HIERARCHIES", "Name": "Column and hierarchy names must start with uppercase letter", "Category": "Naming Conventions", "Description": "Avoid using prefixes and camelCasing. Use \"Sales\" instead of \"dimSales\" or \"mSales\".", "Severity": 2, "Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn", "Expression": "IsVisible\nand char.IsLower(Name[0])\nand (Model.Cultures.Count = 0 or TranslatedNames.Any(it = \"\" or it = outerIt.Name))", "CompatibilityLevel": 1200 }, { "ID": "UPPERCASE_FIRST_LETTER_MEASURES_TABLES", "Name": "Measure and table names must start with uppercase letter", "Category": "Naming Conventions", "Description": "Avoid using prefixes and camelCasing. Use \"Sales\" instead of \"dimSales\" or \"mSales\".", "Severity": 2, "Scope": "Table, Measure, CalculatedTable", "Expression": "IsVisible\nand char.IsLower(Name[0])\nand (Model.Cultures.Count = 0 or TranslatedNames.Any(it = \"\" or it = outerIt.Name))\n", "CompatibilityLevel": 1200 }, { "ID": "AVOID_SINGLE_ATTRIBUTE_DIMENSIONS", "Name": "Avoid single-attribute dimensions that are not shared by multiple facts", "Category": "Performance", "Description": "In general, over-normalization should be avoided. If a dimension only holds a single attribute and the dimension is not shared by multiple facts, consider moving the attribute to the fact table.", "Severity": 2, "Scope": "Table", "Expression": "Columns.Count(IsVisible and not UsedInRelationships.Any()) <= 1 and\nModel.Relationships.Count(ToTable = outerIt) = 1", "CompatibilityLevel": 1200 }, { "ID": "PERF_UNUSED_COLUMNS", "Name": "Remove unused columns", "Category": "Performance", "Description": "Hidden columns, which do not have any dependencies, are not used in any relationships, not used in any hierarchies and not used as the SortByColumn for other columns, will likely not be used by clients and thus take up unnecessary space. Consider removing the columns from the model to save space and improve processing time, if you are certain that no external DAX or MDX queries make use of the columns.", "Severity": 2, "Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn", "Expression": "not IsVisible\n\nand ReferencedBy.Count = 0 \n\nand (not UsedInRelationships.Any())\n\nand (not UsedInSortBy.Any())\n\nand (not UsedInHierarchies.Any())\n\nand (not UsedInVariations.Any())", "FixExpression": "Delete()", "CompatibilityLevel": 1200 }, { "ID": "PERF_UNUSED_MEASURES", "Name": "Remove unused measures", "Category": "Performance", "Description": "Hidden measures, that are not referenced by any DAX expression, should be removed.", "Severity": 1, "Scope": "Measure", "Expression": "not IsVisible\nand ReferencedBy.Count = 0", "FixExpression": "Delete()", "CompatibilityLevel": 1200 }, { "ID": "DIABLE_AUTO_DATE/TIME", "Name": "Diable auto date/time", "Category": "Model Layout", "Description": "Provide your own Calendar tables instead of relying on Power BI Desktop's built-in auto date/time feature (go to File > Options and settings > Options > Current File > Data Load and remove the checkmark from \"Auto date/time\").", "Severity": 3, "Scope": "Model", "Expression": "Tables.Any(HasAnnotation(\"__PBI_LocalDateTable\"))", "CompatibilityLevel": 1200 } ]