This commit is contained in:
Christian Wade 2021-06-02 13:01:46 -07:00
commit 9d1424a148
2 changed files with 103 additions and 23 deletions

View File

@ -28,7 +28,7 @@
"Description": "For best performance, it is recommended to avoid using bi-directional relationships against high-cardinality columns. In order to run this rule, you must first run the script shown here: https://www.elegantbi.com/post/vertipaqintabulareditor", "Description": "For best performance, it is recommended to avoid using bi-directional relationships against high-cardinality columns. In order to run this rule, you must first run the script shown here: https://www.elegantbi.com/post/vertipaqintabulareditor",
"Severity": 2, "Severity": 2,
"Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn", "Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn",
"Expression": "UsedInRelationships.Any(CrossFilteringBehavior == CrossFilteringBehavior.BothDirections)\n\nand\n\nConvert.ToInt32(GetAnnotation(\"Vertipaq_Cardinality\")) > 1000000", "Expression": "UsedInRelationships.Any(CrossFilteringBehavior == CrossFilteringBehavior.BothDirections)\n\nand\n\nConvert.ToInt64(GetAnnotation(\"Vertipaq_Cardinality\")) > 100000",
"CompatibilityLevel": 1200 "CompatibilityLevel": 1200
}, },
{ {
@ -265,7 +265,7 @@
"Description": "The TREATAS function is more efficient and provides better performance than the INTERSECT function when used in virutal relationships.\r\nReference: https://www.sqlbi.com/articles/propagate-filters-using-treatas-in-dax/", "Description": "The TREATAS function is more efficient and provides better performance than the INTERSECT function when used in virutal relationships.\r\nReference: https://www.sqlbi.com/articles/propagate-filters-using-treatas-in-dax/",
"Severity": 2, "Severity": 2,
"Scope": "Measure, CalculationItem", "Scope": "Measure, CalculationItem",
"Expression": "RegEx.IsMatch(Expression.Replace(\" \",\"\"),\"\\W+(?i)INTERSECT\\(\")\r\nor\r\nRegEx.IsMatch(Expression.Replace(\" \",\"\"),\"^(?i)INTERSECT\\(\")", "Expression": "RegEx.IsMatch(Expression,\"(?i)INTERSECT\\s*\\(\")",
"CompatibilityLevel": 1400 "CompatibilityLevel": 1400
}, },
{ {
@ -274,8 +274,8 @@
"Category": "DAX Expressions", "Category": "DAX Expressions",
"Description": "Use the DIVIDE function instead of using \"/\". The DIVIDE function resolves divide-by-zero cases. As such, it is recommended to use to avoid errors. \r\nReference: https://docs.microsoft.com/en-us/power-bi/guidance/dax-divide-function-operator", "Description": "Use the DIVIDE function instead of using \"/\". The DIVIDE function resolves divide-by-zero cases. As such, it is recommended to use to avoid errors. \r\nReference: https://docs.microsoft.com/en-us/power-bi/guidance/dax-divide-function-operator",
"Severity": 2, "Severity": 2,
"Scope": "Measure, CalculatedColumn", "Scope": "Measure, CalculatedColumn, CalculationItem",
"Expression": "Expression.Replace(\" \",\"\").Contains(\"]/\")\n\r\nor\r\n\nExpression.Replace(\" \",\"\").Contains(\"])/\")\n", "Expression": "RegEx.IsMatch(Expression,\"\\]\\s*\\/\")\r\nor\r\nRegEx.IsMatch(Expression,\"\\)\\s*\\/\")",
"CompatibilityLevel": 1200 "CompatibilityLevel": 1200
}, },
{ {
@ -285,7 +285,7 @@
"Description": "Avoid using the IFERROR function as it may cause performance degradation. If you are concerned about a divide-by-zero error, use the DIVIDE function as it naturally resolves such errors as blank (or you can customize what should be shown in case of such an error).\r\nReference: https://www.elegantbi.com/post/top10bestpractices", "Description": "Avoid using the IFERROR function as it may cause performance degradation. If you are concerned about a divide-by-zero error, use the DIVIDE function as it naturally resolves such errors as blank (or you can customize what should be shown in case of such an error).\r\nReference: https://www.elegantbi.com/post/top10bestpractices",
"Severity": 2, "Severity": 2,
"Scope": "Measure, CalculatedColumn", "Scope": "Measure, CalculatedColumn",
"Expression": "RegEx.IsMatch(Expression.Replace(\" \",\"\"),\"\\W+(?i)IFERROR\\(\")\r\nor\r\nRegEx.IsMatch(Expression.Replace(\" \",\"\"),\"^(?i)IFERROR\\(\")", "Expression": "RegEx.IsMatch(Expression,\"(?i)IFERROR\\s*\\(\")",
"CompatibilityLevel": 1200 "CompatibilityLevel": 1200
}, },
{ {
@ -298,6 +298,36 @@
"Expression": "Model.AllMeasures.Any(DaxObjectName == current.Expression)", "Expression": "Model.AllMeasures.Any(DaxObjectName == current.Expression)",
"CompatibilityLevel": 1200 "CompatibilityLevel": 1200
}, },
{
"ID": "FILTER_COLUMN_VALUES",
"Name": "[DAX Expressions] Filter column values with proper syntax",
"Category": "DAX Expressions",
"Description": "Instead of using this pattern FILTER('Table','Table'[Column]=\"Value\") for the filter parameters of a CALCULATE or CALCULATETABLE function, use one of the options below. As far as whether to use the KEEPFILTERS function, see the second reference link below.\r\n\r\nOption 1: KEEPFILTERS('Table'[Column]=\"Value\")\r\nOption 2: 'Table'[Column]=\"Value\"\r\n\r\nReference: https://docs.microsoft.com/power-bi/guidance/dax-avoid-avoid-filter-as-filter-argument\r\nReference: https://www.sqlbi.com/articles/using-keepfilters-in-dax/",
"Severity": 2,
"Scope": "Measure, CalculatedColumn, CalculationItem",
"Expression": "RegEx.IsMatch(Expression,\"(?i)CALCULATE\\s*\\(\\s*[^,]+,\\s*(?i)FILTER\\s*\\(\\s*\\'*[A-Za-z0-9 _]+'*\\s*,\\s*\\'*[A-Za-z0-9 _]+\\'*\\[[A-Za-z0-9 _]+\\]\")\r\nor\r\nRegEx.IsMatch(Expression,\"(?i)CALCULATETABLE\\s*\\([^,]*,\\s*(?i)FILTER\\s*\\(\\s*\\'*[A-Za-z0-9 _]+\\'*,\\s*\\'*[A-Za-z0-9 _]+\\'*\\[[A-Za-z0-9 _]+\\]\")",
"CompatibilityLevel": 1200
},
{
"ID": "FILTER_MEASURE_VALUES_BY_COLUMNS",
"Name": "[DAX Expressions] Filter measure values by columns, not tables",
"Category": "DAX Expressions",
"Description": "Instead of using this pattern FILTER('Table',[Measure]>Value) for the filter parameters of a CALCULATE or CALCULATETABLE function, use one of the options below (if possible). Filtering on a specific column will produce a smaller table for the engine to process, thereby enabling faster performance. Using the VALUES function or the ALL function depends on the desired measure result.\r\n\r\nOption 1: FILTER(VALUES('Table'[Column]),[Measure] > Value)\r\nOption 2: FILTER(ALL('Table'[Column]),[Measure] > Value)\r\n\r\nReference: https://docs.microsoft.com/power-bi/guidance/dax-avoid-avoid-filter-as-filter-argument",
"Severity": 2,
"Scope": "Measure, CalculatedColumn, CalculationItem",
"Expression": "RegEx.IsMatch(Expression,\"(?i)CALCULATE\\s*\\(\\s*[^,]+,\\s*(?i)FILTER\\s*\\(\\s*\\'*[A-Za-z0-9 _]+\\'*\\s*,\\s*\\[[^\\]]+\\]\")\r\nor\r\nRegEx.IsMatch(Expression,\"(?i)CALCULATETABLE\\s*\\([^,]*,\\s*(?i)FILTER\\s*\\(\\s*\\'*[A-Za-z0-9 _]+\\'*,\\s*\\[\")",
"CompatibilityLevel": 1200
},
{
"ID": "INACTIVE_RELATIONSHIPS_THAT_ARE_NEVER_ACTIVATED",
"Name": "[DAX Expressions] Inactive relationships that are never activated",
"Category": "DAX Expressions",
"Description": "Inactive relationships are activated using the USERELATIONSHIP function. If an inactive relationship is not referenced in any measure via this function, the relationship will not be used. It should be determined whether the relationship is not necessary or to activate the relationship via this method.\r\n\r\nReference: https://docs.microsoft.com/power-bi/guidance/relationships-active-inactive\r\nReference: https://dax.guide/userelationship/",
"Severity": 2,
"Scope": "Relationship",
"Expression": "IsActive == false\r\nand not\r\n(\r\nModel.AllMeasures.Any(RegEx.IsMatch(Expression,\r\n\"(?i)USERELATIONSHIP\\s*\\(\\s*\\'*\" +\r\ncurrent.FromTable.Name + \"\\'*\\[\" + \r\ncurrent.FromColumn.Name + \"\\]\\s*,\\s*\\'*\" +\r\ncurrent.ToTable.Name + \"\\'*\\[\" +\r\ncurrent.ToColumn.Name + \"\\]\"))\r\nor\r\nModel.AllCalculationItems.Any(RegEx.IsMatch(Expression,\r\n\"(?i)USERELATIONSHIP\\s*\\(\\s*\\'*\" +\r\ncurrent.FromTable.Name + \"\\'*\\[\" + \r\ncurrent.FromColumn.Name + \"\\]\\s*,\\s*\\'*\" +\r\ncurrent.ToTable.Name + \"\\'*\\[\" +\r\ncurrent.ToColumn.Name + \"\\]\"))\r\n)",
"CompatibilityLevel": 1200
},
{ {
"ID": "DATA_COLUMNS_MUST_HAVE_A_SOURCE_COLUMN", "ID": "DATA_COLUMNS_MUST_HAVE_A_SOURCE_COLUMN",
"Name": "[Error Prevention] Data columns must have a source column", "Name": "[Error Prevention] Data columns must have a source column",
@ -309,12 +339,12 @@
"CompatibilityLevel": 1200 "CompatibilityLevel": 1200
}, },
{ {
"ID": "CALCULATED_COLUMNS_MUST_HAVE_AN_EXPRESSION", "ID": "EXPRESSION_RELIANT_OBJECTS_MUST_HAVE_AN_EXPRESSION",
"Name": "[Error Prevention] Calculated columns must have an expression", "Name": "[Error Prevention] Expression-reliant objects must have an expression",
"Category": "Error Prevention", "Category": "Error Prevention",
"Description": "It is recommended not to use calculated columns. However, if you do, they must have a DAX expression.", "Description": "Calculated columns, calculation items and measures must have an expression. Without an expression, these objects will not show any values.",
"Severity": 3, "Severity": 3,
"Scope": "CalculatedColumn", "Scope": "Measure, CalculatedColumn, CalculationItem",
"Expression": "string.IsNullOrWhiteSpace(Expression)", "Expression": "string.IsNullOrWhiteSpace(Expression)",
"CompatibilityLevel": 1200 "CompatibilityLevel": 1200
}, },
@ -374,12 +404,33 @@
}, },
{ {
"ID": "OBJECTS_WITH_NO_DESCRIPTION", "ID": "OBJECTS_WITH_NO_DESCRIPTION",
"Name": "[Maintenance] Objects with no description", "Name": "[Maintenance] Visible objects with no description",
"Category": "Maintenance", "Category": "Maintenance",
"Description": "Add descriptions to objects. These descriptions are shown on hover within the Field List in Power BI Desktop. Additionally, you can leverage these descriptions to create an automated data dictionary (see link below).\r\nReference: https://www.elegantbi.com/post/datadictionary", "Description": "Add descriptions to objects. These descriptions are shown on hover within the Field List in Power BI Desktop. Additionally, you can leverage these descriptions to create an automated data dictionary (see link below).\r\nReference: https://www.elegantbi.com/post/datadictionary",
"Severity": 1, "Severity": 1,
"Scope": "Table, Measure, DataColumn, CalculatedColumn, CalculatedTable, CalculatedTableColumn, CalculationGroup, CalculationItem", "Scope": "Table, Measure, DataColumn, CalculatedColumn, CalculatedTable, CalculatedTableColumn, CalculationGroup",
"Expression": "string.IsNullOrWhitespace(Description)", "Expression": "string.IsNullOrWhitespace(Description)\r\nand\r\nIsHidden == false",
"CompatibilityLevel": 1200
},
{
"ID": "PERSPECTIVES_WITH_NO_OBJECTS",
"Name": "[Maintenance] Perspectives with no objects",
"Category": "Maintenance",
"Description": "Perspectives that contain no objects (tables) are most likely not necessary. In this rule, it is only necessary to check tables as adding a column/measure/hierarchy to a perspective also adds the table to the perspective. Additionally, tables in general covers calculated tables and calculation groups as well.",
"Severity": 1,
"Scope": "Perspective",
"Expression": "Model.Tables.Any(InPerspective[current.Name]) == false",
"FixExpression": "Delete()",
"CompatibilityLevel": 1200
},
{
"ID": "CALCULATION_GROUPS_WITH_NO_CALCULATION_ITEMS",
"Name": "[Maintenance] Calculation groups with no calculation items",
"Category": "Maintenance",
"Description": "Calculation groups have no function unless they have calculation items.",
"Severity": 2,
"Scope": "CalculationGroup",
"Expression": "CalculationItems.Count == 0",
"CompatibilityLevel": 1200 "CompatibilityLevel": 1200
}, },
{ {
@ -390,6 +441,7 @@
"Severity": 1, "Severity": 1,
"Scope": "Table", "Scope": "Table",
"Expression": "(Partitions.Count = 1 and Partitions[0].Name <> Name)", "Expression": "(Partitions.Count = 1 and Partitions[0].Name <> Name)",
"FixExpression": "Partitions[0].Name = it.Name",
"CompatibilityLevel": 1200 "CompatibilityLevel": 1200
}, },
{ {
@ -399,7 +451,7 @@
"Description": "Tabs, line breaks, etc.", "Description": "Tabs, line breaks, etc.",
"Severity": 2, "Severity": 2,
"Scope": "Model, Table, Measure, Hierarchy, Perspective, Partition, DataColumn, CalculatedColumn, CalculatedTable, CalculatedTableColumn, CalculationGroup, CalculationItem", "Scope": "Model, Table, Measure, Hierarchy, Perspective, Partition, DataColumn, CalculatedColumn, CalculatedTable, CalculatedTableColumn, CalculationGroup, CalculationItem",
"Expression": "Name.IndexOf(char(9)) > -1 or\nName.IndexOf(char(10)) > -1 or\nName.IndexOf(char(13)) > -1", "Expression": "Name.IndexOf(char(9)) > -1\r\nor\r\n\nName.IndexOf(char(10)) > -1 \r\nor\r\n\nName.IndexOf(char(13)) > -1",
"CompatibilityLevel": 1200 "CompatibilityLevel": 1200
}, },
{ {
@ -461,7 +513,7 @@
"Description": "Numeric columns (integer, decimal, double) should have their SummarizeBy property set to \"None\" to avoid accidental summation in Power BI (create measures instead).", "Description": "Numeric columns (integer, decimal, double) should have their SummarizeBy property set to \"None\" to avoid accidental summation in Power BI (create measures instead).",
"Severity": 3, "Severity": 3,
"Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn", "Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn",
"Expression": "(DataType = \"Int64\" or DataType=\"Decimal\" or DataType=\"Double\")\nand SummarizeBy <> \"None\"\nand not (IsHidden or Table.IsHidden)", "Expression": "(\r\nDataType = \"Int64\"\r\nor \r\nDataType=\"Decimal\" \r\nor \r\nDataType=\"Double\"\r\n)\n\r\nand \r\nSummarizeBy <> \"None\"\r\n\nand not (IsHidden or Table.IsHidden)",
"FixExpression": "SummarizeBy = AggregateFunction.None", "FixExpression": "SummarizeBy = AggregateFunction.None",
"CompatibilityLevel": 1200 "CompatibilityLevel": 1200
}, },
@ -512,7 +564,7 @@
"Description": "Foreign keys should always be hidden.", "Description": "Foreign keys should always be hidden.",
"Severity": 2, "Severity": 2,
"Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn", "Scope": "DataColumn, CalculatedColumn, CalculatedTableColumn",
"Expression": "UsedInRelationships.Any(FromColumn.Name == current.Name and FromCardinality == \"Many\")\nand\r\n\nIsHidden == false", "Expression": "UsedInRelationships.Any(FromColumn.Name == current.Name and FromCardinality == \"Many\")\n\r\nand\r\n\nIsHidden == false",
"FixExpression": "IsHidden = true", "FixExpression": "IsHidden = true",
"CompatibilityLevel": 1200 "CompatibilityLevel": 1200
}, },

View File

@ -2,6 +2,8 @@
Make sure to also check out the [PowerBI.com blog post](https://powerbi.microsoft.com/en-us/blog/best-practice-rules-to-improve-your-models-performance/ "PowerBI.com blog post") on this topic! Make sure to also check out the [PowerBI.com blog post](https://powerbi.microsoft.com/en-us/blog/best-practice-rules-to-improve-your-models-performance/ "PowerBI.com blog post") on this topic!
And, check out the new [PowerBI.com blog post on v1.1](https://powerbi.microsoft.com/en-us/blog/best-practice-rules-to-improve-your-models-performance-and-design-v1-1/, "PowerBI.com blog post").
Also, check out this [post](https://www.elegantbi.com/post/bestpracticerulesavings "Best Practice Rule Savings") for quantifying the savings of following specific Best Practice Rules. Also, check out this [post](https://www.elegantbi.com/post/bestpracticerulesavings "Best Practice Rule Savings") for quantifying the savings of following specific Best Practice Rules.
## Purpose ## Purpose
@ -22,13 +24,15 @@ Following these steps will automatically load the Best Practice Rules into your
2. Connect to a model. 2. Connect to a model.
3. Run the following code in the Advanced Scripting window. 3. Run the following code in the Advanced Scripting window.
System.Net.WebClient w = new System.Net.WebClient(); ```C#
System.Net.WebClient w = new System.Net.WebClient();
string path = System.Environment.GetFolderPath(System.Environment.SpecialFolder.LocalApplicationData); string path = System.Environment.GetFolderPath(System.Environment.SpecialFolder.LocalApplicationData);
string url = "https://raw.githubusercontent.com/microsoft/Analysis-Services/master/BestPracticeRules/BPARules.json"; string url = "https://raw.githubusercontent.com/microsoft/Analysis-Services/master/BestPracticeRules/BPARules.json";
string downloadLoc = path+@"\TabularEditor\BPARules.json"; string downloadLoc = path+@"\TabularEditor\BPARules.json";
w.DownloadFile(url, downloadLoc); w.DownloadFile(url, downloadLoc);
```
4. Close and reopen [Tabular Editor](https://tabulareditor.com/ "Tabular Editor"). 4. Close and reopen [Tabular Editor](https://tabulareditor.com/ "Tabular Editor").
5. Connect to a model. 5. Connect to a model.
@ -66,4 +70,28 @@ Following these steps will automatically load the Best Practice Rules into your
## Requirements ## Requirements
[Tabular Editor](https://tabulareditor.com/ "Tabular Editor") version 2.12.1 or higher. [Tabular Editor](https://tabulareditor.com/ "Tabular Editor") version 2.16.1 or higher.
## Version History
* 2021-05-26 Version 1.1.1
* Modified Rules
* [DAX Expressions] Inactive relationships that are never activated
* Expanded the scope to include Calculation Items ([#110](https://github.com/microsoft/Analysis-Services/issues/110))
* 2021-05-20 Version 1.1 (make sure to read the [blog post](https://powerbi.microsoft.com/en-us/blog/best-practice-rules-to-improve-your-models-performance-and-design-v1-1/ "blog post"))
* New Rules
* [DAX Expressions] Filter column values with proper syntax
* [DAX Expressions] Fllter measure values by columns, not tables
* [DAX Expressions] Inactive relationships that are never activated
* [Maintenance] Perspectives with no objects
* [Maintenance] Calculation groups with no calculation items
* Modified Rules
* [Naming Conventions] Partition name should match table name for single partition tables
* Added Fix Expression (must use Tabular Editor 2.16.1 or higher)
* [Error Prevention] Calculated columns must have an expression
* New name: Expression-reliant objects must have an expression
* [Maintenance] Objects with no description
* New name: Visible objects with no description
* Removed Rules
* [DAX Expressions] No two measures should have the same definition
* 2021-02-03 Version 1.0